mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
Merge pull request #1051 from System233/patch-settings-panel-rename
Fix settings panel reset during state update
This commit is contained in:
commit
e6f3bb7051
16 changed files with 639 additions and 610 deletions
|
|
@ -2236,18 +2236,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
|
||||
if (response.data) {
|
||||
const rawModels: Record<string, any> = response.data
|
||||
|
||||
for (const [modelId, model] of Object.entries(rawModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: model.maxTokens ? parseInt(model.maxTokens) : undefined,
|
||||
contextWindow: model.contextWindow ? parseInt(model.contextWindow) : 0,
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
supportsPromptCache: model.supportsPromptCaching ?? false,
|
||||
supportsComputerUse: model.supportsComputerUse ?? false,
|
||||
inputPrice: model.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
|
||||
outputPrice: model.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
|
||||
cacheWritesPrice: model.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
|
||||
cacheReadsPrice: model.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
|
||||
maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined,
|
||||
contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0,
|
||||
supportsImages: model?.supportsImages ?? false,
|
||||
supportsPromptCache: model?.supportsPromptCaching ?? false,
|
||||
supportsComputerUse: model?.supportsComputerUse ?? false,
|
||||
inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
|
||||
outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
|
||||
cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
|
||||
cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
|
|||
import { Dropdown } from "vscrui"
|
||||
import type { DropdownOption } from "vscrui"
|
||||
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog"
|
||||
import { Button, Input } from "../ui"
|
||||
|
||||
interface ApiConfigManagerProps {
|
||||
currentApiConfigName?: string
|
||||
|
|
@ -299,10 +300,7 @@ const ApiConfigManager = ({
|
|||
aria-labelledby="new-profile-title">
|
||||
<DialogContent className="p-4 max-w-sm">
|
||||
<DialogTitle>New Configuration Profile</DialogTitle>
|
||||
<button className="absolute right-4 top-4" aria-label="Close dialog" onClick={resetCreateState}>
|
||||
<span className="codicon codicon-close" />
|
||||
</button>
|
||||
<VSCodeTextField
|
||||
<Input
|
||||
ref={newProfileInputRef}
|
||||
value={newProfileName}
|
||||
onInput={(e: unknown) => {
|
||||
|
|
@ -327,15 +325,12 @@ const ApiConfigManager = ({
|
|||
</p>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<VSCodeButton appearance="secondary" onClick={resetCreateState}>
|
||||
<Button variant="secondary" onClick={resetCreateState}>
|
||||
Cancel
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!newProfileName.trim()}
|
||||
onClick={handleNewProfileSave}>
|
||||
</Button>
|
||||
<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
|
||||
Create Profile
|
||||
</VSCodeButton>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { memo, useCallback, useMemo, useState } from "react"
|
||||
import { useDebounce, useEvent } from "react-use"
|
||||
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
|
||||
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { TemperatureControl } from "./TemperatureControl"
|
||||
|
|
@ -34,7 +34,6 @@ import {
|
|||
requestyDefaultModelInfo,
|
||||
} from "../../../../src/shared/api"
|
||||
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import VSCodeButtonLink from "../common/VSCodeButtonLink"
|
||||
import { OpenRouterModelPicker } from "./OpenRouterModelPicker"
|
||||
|
|
@ -46,13 +45,22 @@ import { DROPDOWN_Z_INDEX } from "./styles"
|
|||
import { RequestyModelPicker } from "./RequestyModelPicker"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
uriScheme: string | undefined
|
||||
apiConfiguration: ApiConfiguration | undefined
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
|
||||
apiErrorMessage?: string
|
||||
modelIdErrorMessage?: string
|
||||
fromWelcomeView?: boolean
|
||||
}
|
||||
|
||||
const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: ApiOptionsProps) => {
|
||||
const { apiConfiguration, uriScheme, handleInputChange } = useExtensionState()
|
||||
const ApiOptions = ({
|
||||
uriScheme,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
apiErrorMessage,
|
||||
modelIdErrorMessage,
|
||||
fromWelcomeView,
|
||||
}: ApiOptionsProps) => {
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
|
|
@ -61,33 +69,40 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const inputEventTransform = <E,>(event: E) => (event as { target: HTMLInputElement })?.target?.value as any
|
||||
const noTransform = <T,>(value: T) => value
|
||||
const dropdownEventTransform = <T,>(event: DropdownOption | string | undefined) =>
|
||||
(typeof event == "string" ? event : event?.value) as T
|
||||
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 { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
const requestLocalModelsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
// Pull ollama/lmstudio models
|
||||
const requestLocalModels = useCallback(() => {
|
||||
if (selectedProvider === "ollama") {
|
||||
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
|
||||
} else if (selectedProvider === "lmstudio") {
|
||||
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
|
||||
} else if (selectedProvider === "vscode-lm") {
|
||||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
}
|
||||
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
|
||||
// Debounced model updates, only executed 250ms after the user stops typing
|
||||
useEffect(() => {
|
||||
if (requestLocalModelsTimeoutRef.current) {
|
||||
clearTimeout(requestLocalModelsTimeoutRef.current)
|
||||
}
|
||||
requestLocalModelsTimeoutRef.current = setTimeout(requestLocalModels, 250)
|
||||
return () => {
|
||||
if (requestLocalModelsTimeoutRef.current) {
|
||||
clearTimeout(requestLocalModelsTimeoutRef.current)
|
||||
useDebounce(
|
||||
() => {
|
||||
if (selectedProvider === "ollama") {
|
||||
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
|
||||
} else if (selectedProvider === "lmstudio") {
|
||||
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
|
||||
} else if (selectedProvider === "vscode-lm") {
|
||||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
}
|
||||
}
|
||||
}, [requestLocalModels])
|
||||
},
|
||||
250,
|
||||
[selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl],
|
||||
)
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
|
||||
|
|
@ -115,12 +130,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
<Dropdown
|
||||
id="model-id"
|
||||
value={selectedModelId}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("apiModelId")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
})
|
||||
onChange={(value) => {
|
||||
setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
options={options}
|
||||
|
|
@ -137,13 +148,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
<Dropdown
|
||||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("apiProvider")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("apiProvider", dropdownEventTransform)}
|
||||
style={{ minWidth: 130, position: "relative", zIndex: DROPDOWN_Z_INDEX + 1 }}
|
||||
options={[
|
||||
{ value: "openrouter", label: "OpenRouter" },
|
||||
|
|
@ -181,11 +186,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
onChange={(checked: boolean) => {
|
||||
setAnthropicBaseUrlSelected(checked)
|
||||
if (!checked) {
|
||||
handleInputChange("anthropicBaseUrl")({
|
||||
target: {
|
||||
value: "",
|
||||
},
|
||||
})
|
||||
setApiConfigurationField("anthropicBaseUrl", "")
|
||||
}
|
||||
}}>
|
||||
Use custom base URL
|
||||
|
|
@ -384,11 +385,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
onChange={(checked: boolean) => {
|
||||
setOpenRouterBaseUrlSelected(checked)
|
||||
if (!checked) {
|
||||
handleInputChange("openRouterBaseUrl")({
|
||||
target: {
|
||||
value: "",
|
||||
},
|
||||
})
|
||||
setApiConfigurationField("openRouterBaseUrl", "")
|
||||
}
|
||||
}}>
|
||||
Use custom base URL
|
||||
|
|
@ -405,11 +402,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
)}
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openRouterUseMiddleOutTransform")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
onChange={handleInputChange("openRouterUseMiddleOutTransform", noTransform)}>
|
||||
Compress prompts and message chains to the context size (
|
||||
<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
|
||||
</Checkbox>
|
||||
|
|
@ -422,13 +415,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<VSCodeRadioGroup
|
||||
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
|
||||
onChange={(e) => {
|
||||
const value = (e.target as HTMLInputElement)?.value
|
||||
const useProfile = value === "profile"
|
||||
handleInputChange("awsUseProfile")({
|
||||
target: { value: useProfile },
|
||||
})
|
||||
}}>
|
||||
onChange={handleInputChange(
|
||||
"awsUseProfile",
|
||||
(e) => (e.target as HTMLInputElement).value === "profile",
|
||||
)}>
|
||||
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
|
||||
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
|
|
@ -479,11 +469,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
value={apiConfiguration?.awsRegion || ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("awsRegion")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
})
|
||||
handleInputChange("awsRegion", dropdownEventTransform)
|
||||
}}
|
||||
options={[
|
||||
{ value: "", label: "Select a region..." },
|
||||
|
|
@ -507,11 +493,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
</div>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.awsUseCrossRegionInference || false}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("awsUseCrossRegionInference")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
onChange={handleInputChange("awsUseCrossRegionInference", noTransform)}>
|
||||
Use cross-region inference
|
||||
</Checkbox>
|
||||
<p
|
||||
|
|
@ -544,13 +526,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
id="vertex-region-dropdown"
|
||||
value={apiConfiguration?.vertexRegion || ""}
|
||||
style={{ width: "100%" }}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("vertexRegion")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("vertexRegion", dropdownEventTransform)}
|
||||
options={[
|
||||
{ value: "", label: "Select a region..." },
|
||||
{ value: "us-east5", label: "us-east5" },
|
||||
|
|
@ -634,21 +610,13 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openAiStreamingEnabled")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
|
||||
Enable streaming
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiUseAzure ?? false}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openAiUseAzure")({
|
||||
target: { value: checked },
|
||||
})
|
||||
}}>
|
||||
onChange={handleInputChange("openAiUseAzure", noTransform)}>
|
||||
Use Azure
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
|
|
@ -656,11 +624,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
onChange={(checked: boolean) => {
|
||||
setAzureApiVersionSelected(checked)
|
||||
if (!checked) {
|
||||
handleInputChange("azureApiVersion")({
|
||||
target: {
|
||||
value: "",
|
||||
},
|
||||
})
|
||||
setApiConfigurationField("azureApiVersion", "")
|
||||
}
|
||||
}}>
|
||||
Set Azure API version
|
||||
|
|
@ -686,15 +650,12 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
{
|
||||
iconName: "refresh",
|
||||
onClick: () =>
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: { value: openAiModelInfoSaneDefaults },
|
||||
}),
|
||||
setApiConfigurationField("openAiCustomModelInfo", openAiModelInfoSaneDefaults),
|
||||
},
|
||||
]}>
|
||||
<div
|
||||
style={{
|
||||
padding: 15,
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
padding: 12,
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -708,24 +669,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
</p>
|
||||
|
||||
{/* Capabilities Section */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
padding: 12,
|
||||
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
||||
borderRadius: 4,
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "12px",
|
||||
display: "block",
|
||||
marginBottom: 12,
|
||||
color: "var(--vscode-editor-foreground)",
|
||||
}}>
|
||||
<div>
|
||||
<h3 className="font-medium text-sm text-vscode-editor-foreground">
|
||||
Model Capabilities
|
||||
</span>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="token-config-field">
|
||||
<VSCodeTextField
|
||||
value={
|
||||
|
|
@ -745,18 +693,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
})(),
|
||||
}}
|
||||
title="Maximum number of tokens the model can generate in a single response"
|
||||
onChange={(e: any) => {
|
||||
const value = parseInt(e.target.value)
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
maxTokens: isNaN(value) ? undefined : value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = parseInt((e.target as HTMLInputElement).value)
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
maxTokens: isNaN(value) ? undefined : value,
|
||||
}
|
||||
})}
|
||||
placeholder="e.g. 4096">
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
|
|
@ -796,23 +740,17 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
})(),
|
||||
}}
|
||||
title="Total number of tokens (input + output) the model can process in a single request"
|
||||
onChange={(e: any) => {
|
||||
const parsed = parseInt(e.target.value)
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
contextWindow:
|
||||
e.target.value === ""
|
||||
? undefined
|
||||
: isNaN(parsed)
|
||||
? openAiModelInfoSaneDefaults.contextWindow
|
||||
: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={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="e.g. 128000">
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
|
|
@ -832,160 +770,104 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
padding: "12px",
|
||||
borderRadius: "4px",
|
||||
marginTop: "8px",
|
||||
border: "1px solid var(--vscode-input-border)",
|
||||
transition: "background-color 0.2s ease",
|
||||
}}>
|
||||
<span
|
||||
<div>
|
||||
<h3 className="font-medium text-sm text-vscode-editor-foreground">Model Features</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="feature-toggle">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Checkbox
|
||||
checked={
|
||||
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
|
||||
openAiModelInfoSaneDefaults.supportsImages
|
||||
}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
supportsImages: checked,
|
||||
}
|
||||
})}>
|
||||
<span style={{ fontWeight: 500 }}>Image Support</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Enable if the model can process and understand images in the input. Required for image-based assistance and visual code understanding."
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
cursor: "help",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
color: "var(--vscode-editor-foreground)",
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginLeft: "24px",
|
||||
marginTop: "4px",
|
||||
lineHeight: "1.4",
|
||||
marginBottom: 0,
|
||||
}}>
|
||||
Model Features
|
||||
</span>
|
||||
Allows the model to analyze and understand images, essential for visual code
|
||||
assistance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||||
<div className="feature-toggle">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Checkbox
|
||||
checked={
|
||||
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
|
||||
openAiModelInfoSaneDefaults.supportsImages
|
||||
}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
supportsImages: checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Image Support</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Enable if the model can process and understand images in the input. Required for image-based assistance and visual code understanding."
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
cursor: "help",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginLeft: "24px",
|
||||
marginTop: "4px",
|
||||
lineHeight: "1.4",
|
||||
}}>
|
||||
Allows the model to analyze and understand images, essential for
|
||||
visual code assistance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="feature-toggle"
|
||||
<div
|
||||
className="feature-toggle"
|
||||
style={{
|
||||
borderTop: "1px solid var(--vscode-input-border)",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Checkbox
|
||||
checked={
|
||||
apiConfiguration?.openAiCustomModelInfo?.supportsComputerUse ??
|
||||
false
|
||||
}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
supportsComputerUse: checked,
|
||||
}
|
||||
})}>
|
||||
<span style={{ fontWeight: 500 }}>Computer Use</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Enable if the model can interact with your computer through commands and file operations. Required for automated tasks and file modifications."
|
||||
style={{
|
||||
borderTop: "1px solid var(--vscode-input-border)",
|
||||
paddingTop: "12px",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Checkbox
|
||||
checked={
|
||||
apiConfiguration?.openAiCustomModelInfo
|
||||
?.supportsComputerUse ?? false
|
||||
}
|
||||
onChange={(checked: boolean) => {
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
supportsComputerUse: checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}>
|
||||
<span style={{ fontWeight: 500 }}>Computer Use</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Enable if the model can interact with your computer through commands and file operations. Required for automated tasks and file modifications."
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
cursor: "help",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginLeft: "24px",
|
||||
marginTop: "4px",
|
||||
lineHeight: "1.4",
|
||||
}}>
|
||||
This model feature is for computer use like sonnet 3.5 support
|
||||
</p>
|
||||
</div>
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
cursor: "help",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginLeft: "24px",
|
||||
marginTop: "4px",
|
||||
lineHeight: "1.4",
|
||||
marginBottom: 0,
|
||||
}}>
|
||||
This model feature is for computer use like sonnet 3.5 support
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
||||
padding: "12px",
|
||||
borderRadius: "4px",
|
||||
marginTop: "15px",
|
||||
}}>
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-editor-foreground)",
|
||||
display: "block",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
Model Pricing
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
display: "block",
|
||||
}}>
|
||||
Configure token-based pricing in USD per million tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "12px",
|
||||
backgroundColor: "var(--vscode-editor-background)",
|
||||
padding: "12px",
|
||||
borderRadius: "4px",
|
||||
}}>
|
||||
<div>
|
||||
<h3 className="font-medium text-sm text-vscode-editor-foreground mb-0">
|
||||
Model Pricing
|
||||
</h3>
|
||||
<div className="text-xs">Configure token-based pricing in USD per million tokens</div>
|
||||
<div className="flex flex-row gap-2 mt-1.5">
|
||||
<div className="price-input">
|
||||
<VSCodeTextField
|
||||
value={
|
||||
|
|
@ -1004,23 +886,17 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={(e: any) => {
|
||||
const parsed = parseFloat(e.target.value)
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ??
|
||||
openAiModelInfoSaneDefaults),
|
||||
inputPrice:
|
||||
e.target.value === ""
|
||||
? undefined
|
||||
: isNaN(parsed)
|
||||
? openAiModelInfoSaneDefaults.inputPrice
|
||||
: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseInt(value)
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ??
|
||||
openAiModelInfoSaneDefaults),
|
||||
inputPrice: isNaN(parsed)
|
||||
? openAiModelInfoSaneDefaults.inputPrice
|
||||
: parsed,
|
||||
}
|
||||
})}
|
||||
placeholder="e.g. 0.0001">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price</span>
|
||||
|
|
@ -1055,23 +931,17 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={(e: any) => {
|
||||
const parsed = parseFloat(e.target.value)
|
||||
handleInputChange("openAiCustomModelInfo")({
|
||||
target: {
|
||||
value: {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
outputPrice:
|
||||
e.target.value === ""
|
||||
? undefined
|
||||
: isNaN(parsed)
|
||||
? openAiModelInfoSaneDefaults.outputPrice
|
||||
: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseInt(value)
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ||
|
||||
openAiModelInfoSaneDefaults),
|
||||
outputPrice: isNaN(parsed)
|
||||
? openAiModelInfoSaneDefaults.outputPrice
|
||||
: parsed,
|
||||
}
|
||||
})}
|
||||
placeholder="e.g. 0.0002">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price</span>
|
||||
|
|
@ -1137,15 +1007,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
? apiConfiguration?.lmStudioModelId
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
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 },
|
||||
})
|
||||
}
|
||||
}}>
|
||||
onChange={handleInputChange("lmStudioModelId")}>
|
||||
{lmStudioModels.map((model) => (
|
||||
<VSCodeRadio
|
||||
key={model}
|
||||
|
|
@ -1224,18 +1086,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
|
||||
: ""
|
||||
}
|
||||
onChange={(value: unknown) => {
|
||||
const valueStr = (value as DropdownOption)?.value
|
||||
if (!valueStr) {
|
||||
return
|
||||
}
|
||||
onChange={handleInputChange("vsCodeLmModelSelector", (e) => {
|
||||
const valueStr = (e as DropdownOption)?.value
|
||||
const [vendor, family] = valueStr.split("/")
|
||||
handleInputChange("vsCodeLmModelSelector")({
|
||||
target: {
|
||||
value: { vendor, family },
|
||||
},
|
||||
})
|
||||
}}
|
||||
return { vendor, family }
|
||||
})}
|
||||
style={{ width: "100%" }}
|
||||
options={[
|
||||
{ value: "", label: "Select a model..." },
|
||||
|
|
@ -1296,15 +1151,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
? apiConfiguration?.ollamaModelId
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = (e.target as HTMLInputElement)?.value
|
||||
// need to check value first since radio group returns empty string sometimes
|
||||
if (value) {
|
||||
handleInputChange("ollamaModelId")({
|
||||
target: { value },
|
||||
})
|
||||
}
|
||||
}}>
|
||||
onChange={handleInputChange("ollamaModelId")}>
|
||||
{ollamaModels.map((model) => (
|
||||
<VSCodeRadio
|
||||
key={model}
|
||||
|
|
@ -1373,6 +1220,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
fontSize: 12,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{apiErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -1416,11 +1264,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
<div style={{ marginTop: "10px" }}>
|
||||
<TemperatureControl
|
||||
value={apiConfiguration?.modelTemperature}
|
||||
onChange={(value) => {
|
||||
handleInputChange("modelTemperature")({
|
||||
target: { value },
|
||||
})
|
||||
}}
|
||||
onChange={handleInputChange("modelTemperature", noTransform)}
|
||||
maxValue={2}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1433,6 +1277,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
fontSize: 12,
|
||||
color: "var(--vscode-errorForeground)",
|
||||
}}>
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{modelIdErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,78 +1,137 @@
|
|||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "./ApiOptions"
|
||||
import ExperimentalFeature from "./ExperimentalFeature"
|
||||
import { EXPERIMENT_IDS, experimentConfigsMap } from "../../../../src/shared/experiments"
|
||||
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
|
||||
import ApiConfigManager from "./ApiConfigManager"
|
||||
import { Dropdown } from "vscrui"
|
||||
import type { DropdownOption } from "vscrui"
|
||||
import { ApiConfiguration } from "../../../../src/shared/api"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogCancel,
|
||||
AlertDialogAction,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
} from "../ui/alert-dialog"
|
||||
|
||||
type SettingsViewProps = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
const {
|
||||
apiConfiguration,
|
||||
version,
|
||||
alwaysAllowReadOnly,
|
||||
setAlwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
setAlwaysAllowWrite,
|
||||
alwaysAllowExecute,
|
||||
setAlwaysAllowExecute,
|
||||
alwaysAllowBrowser,
|
||||
setAlwaysAllowBrowser,
|
||||
alwaysAllowMcp,
|
||||
setAlwaysAllowMcp,
|
||||
soundEnabled,
|
||||
setSoundEnabled,
|
||||
soundVolume,
|
||||
setSoundVolume,
|
||||
diffEnabled,
|
||||
setDiffEnabled,
|
||||
checkpointsEnabled,
|
||||
setCheckpointsEnabled,
|
||||
browserViewportSize,
|
||||
setBrowserViewportSize,
|
||||
openRouterModels,
|
||||
glamaModels,
|
||||
setAllowedCommands,
|
||||
allowedCommands,
|
||||
fuzzyMatchThreshold,
|
||||
setFuzzyMatchThreshold,
|
||||
writeDelayMs,
|
||||
setWriteDelayMs,
|
||||
screenshotQuality,
|
||||
setScreenshotQuality,
|
||||
terminalOutputLineLimit,
|
||||
setTerminalOutputLineLimit,
|
||||
mcpEnabled,
|
||||
alwaysApproveResubmit,
|
||||
setAlwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
setRequestDelaySeconds,
|
||||
rateLimitSeconds,
|
||||
setRateLimitSeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
experiments,
|
||||
setExperimentEnabled,
|
||||
alwaysAllowModeSwitch,
|
||||
setAlwaysAllowModeSwitch,
|
||||
maxOpenTabsContext,
|
||||
setMaxOpenTabsContext,
|
||||
} = useExtensionState()
|
||||
const extensionState = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
|
||||
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
|
||||
|
||||
// TODO: Reduce WebviewMessage/ExtensionState complexity
|
||||
const [cachedState, setCachedState] = useState(extensionState)
|
||||
const [isChangeDetected, setChangeDetected] = useState(false)
|
||||
const { currentApiConfigName } = extensionState
|
||||
const {
|
||||
apiConfiguration,
|
||||
alwaysAllowReadOnly,
|
||||
allowedCommands,
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowMcp,
|
||||
alwaysAllowModeSwitch,
|
||||
alwaysAllowWrite,
|
||||
alwaysApproveResubmit,
|
||||
browserViewportSize,
|
||||
checkpointsEnabled,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
fuzzyMatchThreshold,
|
||||
maxOpenTabsContext,
|
||||
mcpEnabled,
|
||||
rateLimitSeconds,
|
||||
requestDelaySeconds,
|
||||
screenshotQuality,
|
||||
soundEnabled,
|
||||
soundVolume,
|
||||
terminalOutputLineLimit,
|
||||
writeDelayMs,
|
||||
} = cachedState
|
||||
|
||||
useEffect(() => {
|
||||
// Update only when currentApiConfigName is changed
|
||||
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration
|
||||
if (prevApiConfigName.current === currentApiConfigName) {
|
||||
return
|
||||
}
|
||||
setCachedState((prevCachedState) => ({
|
||||
...prevCachedState,
|
||||
...extensionState,
|
||||
}))
|
||||
prevApiConfigName.current = currentApiConfigName
|
||||
setChangeDetected(false)
|
||||
}, [currentApiConfigName, extensionState, isChangeDetected])
|
||||
|
||||
const setCachedStateField = useCallback(
|
||||
<K extends keyof ExtensionStateContextType>(field: K, value: ExtensionStateContextType[K]) => {
|
||||
setCachedState((prevState) => {
|
||||
if (prevState[field] === value) {
|
||||
return prevState
|
||||
}
|
||||
setChangeDetected(true)
|
||||
return {
|
||||
...prevState,
|
||||
[field]: value,
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setApiConfigurationField = useCallback(
|
||||
<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
|
||||
setCachedState((prevState) => {
|
||||
if (prevState.apiConfiguration?.[field] === value) {
|
||||
return prevState
|
||||
}
|
||||
setChangeDetected(true)
|
||||
return {
|
||||
...prevState,
|
||||
apiConfiguration: {
|
||||
...prevState.apiConfiguration,
|
||||
[field]: value,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
|
||||
setCachedState((prevState) => {
|
||||
if (prevState.experiments?.[id] === enabled) {
|
||||
return prevState
|
||||
}
|
||||
setChangeDetected(true)
|
||||
return {
|
||||
...prevState,
|
||||
experiments: { ...prevState.experiments, [id]: enabled },
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||
const modelIdValidationResult = validateModelId(
|
||||
apiConfiguration,
|
||||
extensionState.glamaModels,
|
||||
extensionState.openRouterModels,
|
||||
)
|
||||
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
|
|
@ -98,19 +157,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
|
||||
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
|
||||
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
|
||||
vscode.postMessage({
|
||||
type: "updateExperimental",
|
||||
values: experiments,
|
||||
})
|
||||
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
|
||||
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
|
||||
vscode.postMessage({
|
||||
type: "updateExperimental",
|
||||
values: experiments,
|
||||
})
|
||||
|
||||
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
|
||||
onDone()
|
||||
// onDone()
|
||||
setChangeDetected(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,10 +181,32 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
// Initial validation on mount
|
||||
useEffect(() => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||
const modelIdValidationResult = validateModelId(
|
||||
apiConfiguration,
|
||||
extensionState.glamaModels,
|
||||
extensionState.openRouterModels,
|
||||
)
|
||||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
}, [apiConfiguration, glamaModels, openRouterModels])
|
||||
}, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
|
||||
|
||||
const confirmDialogHandler = useRef<() => void>()
|
||||
const onConfirmDialogResult = useCallback((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
confirmDialogHandler.current?.()
|
||||
}
|
||||
}, [])
|
||||
const checkUnsaveChanges = useCallback(
|
||||
(then: () => void) => {
|
||||
if (isChangeDetected) {
|
||||
confirmDialogHandler.current = then
|
||||
setDiscardDialogShow(true)
|
||||
} else {
|
||||
then()
|
||||
}
|
||||
},
|
||||
[isChangeDetected],
|
||||
)
|
||||
|
||||
const handleResetState = () => {
|
||||
vscode.postMessage({ type: "resetState" })
|
||||
|
|
@ -135,7 +216,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
const currentCommands = allowedCommands ?? []
|
||||
if (commandInput && !currentCommands.includes(commandInput)) {
|
||||
const newCommands = [...currentCommands, commandInput]
|
||||
setAllowedCommands(newCommands)
|
||||
setCachedStateField("allowedCommands", newCommands)
|
||||
setCommandInput("")
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
|
|
@ -151,13 +232,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
paddingBottom: "2px",
|
||||
}
|
||||
|
||||
const sliderStyle = {
|
||||
flexGrow: 1,
|
||||
maxWidth: "80%",
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -171,6 +245,21 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-warning align-middle mr-1`} />
|
||||
Do you want to discard changes and continue?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
|
||||
<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -180,7 +269,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
paddingRight: 17,
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
|
||||
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: "6px",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
title={isChangeDetected ? "Save changes" : "Nothing changed"}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isChangeDetected}>
|
||||
Save
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
title="Discard unsaved changes and close settings panel"
|
||||
onClick={() => checkUnsaveChanges(onDone)}>
|
||||
Done
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||
|
|
@ -189,16 +297,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<ApiConfigManager
|
||||
currentApiConfigName={currentApiConfigName}
|
||||
listApiConfigMeta={listApiConfigMeta}
|
||||
listApiConfigMeta={extensionState.listApiConfigMeta}
|
||||
onSelectConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "saveApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: configName,
|
||||
checkUnsaveChanges(() => {
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: configName,
|
||||
})
|
||||
})
|
||||
}}
|
||||
onDeleteConfig={(configName: string) => {
|
||||
|
|
@ -213,6 +318,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
values: { oldName, newName },
|
||||
apiConfiguration,
|
||||
})
|
||||
prevApiConfigName.current = newName
|
||||
}}
|
||||
onUpsertConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
|
|
@ -222,7 +328,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
})
|
||||
}}
|
||||
/>
|
||||
<ApiOptions apiErrorMessage={apiErrorMessage} modelIdErrorMessage={modelIdErrorMessage} />
|
||||
<ApiOptions
|
||||
uriScheme={extensionState.uriScheme}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
apiErrorMessage={apiErrorMessage}
|
||||
modelIdErrorMessage={modelIdErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -237,7 +349,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowReadOnly}
|
||||
onChange={(e: any) => setAlwaysAllowReadOnly(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowReadOnly", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
|
|
@ -254,7 +366,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowWrite}
|
||||
onChange={(e: any) => setAlwaysAllowWrite(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowWrite", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve write operations</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -274,12 +386,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="5000"
|
||||
step="100"
|
||||
value={writeDelayMs}
|
||||
onChange={(e) => setWriteDelayMs(parseInt(e.target.value))}
|
||||
style={{
|
||||
flex: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}}
|
||||
onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
|
||||
</div>
|
||||
|
|
@ -298,7 +406,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowBrowser}
|
||||
onChange={(e: any) => setAlwaysAllowBrowser(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowBrowser", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -311,7 +419,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysApproveResubmit}
|
||||
onChange={(e: any) => setAlwaysApproveResubmit(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysApproveResubmit", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always retry failed API requests</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -331,12 +439,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="100"
|
||||
step="1"
|
||||
value={requestDelaySeconds}
|
||||
onChange={(e) => setRequestDelaySeconds(parseInt(e.target.value))}
|
||||
style={{
|
||||
flex: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}}
|
||||
onChange={(e) =>
|
||||
setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
|
||||
}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
|
||||
</div>
|
||||
|
|
@ -355,7 +461,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowMcp}
|
||||
onChange={(e: any) => setAlwaysAllowMcp(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowMcp", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -367,7 +473,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowModeSwitch}
|
||||
onChange={(e: any) => setAlwaysAllowModeSwitch(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve mode switching & task creation</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -379,7 +485,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowExecute}
|
||||
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
||||
onChange={(e: any) => setCachedStateField("alwaysAllowExecute", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
|
@ -430,35 +536,16 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
{(allowedCommands ?? []).map((cmd, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
backgroundColor: "var(--vscode-button-secondaryBackground)",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid var(--vscode-button-secondaryBorder)",
|
||||
height: "24px",
|
||||
}}>
|
||||
className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
|
||||
<span>{cmd}</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: "20px",
|
||||
width: "20px",
|
||||
minWidth: "20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--vscode-button-foreground)",
|
||||
}}
|
||||
className="text-primary-foreground"
|
||||
onClick={() => {
|
||||
const newCommands = (allowedCommands ?? []).filter(
|
||||
(_, i) => i !== index,
|
||||
)
|
||||
setAllowedCommands(newCommands)
|
||||
setCachedStateField("allowedCommands", newCommands)
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
commands: newCommands,
|
||||
|
|
@ -482,7 +569,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<Dropdown
|
||||
value={browserViewportSize}
|
||||
onChange={(value: unknown) => {
|
||||
setBrowserViewportSize((value as DropdownOption).value)
|
||||
setCachedStateField("browserViewportSize", (value as DropdownOption).value)
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
options={[
|
||||
|
|
@ -514,10 +601,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="100"
|
||||
step="1"
|
||||
value={screenshotQuality ?? 75}
|
||||
onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
|
||||
style={{
|
||||
...sliderStyle,
|
||||
}}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
onChange={(e) => setCachedStateField("screenshotQuality", parseInt(e.target.value))}
|
||||
/>
|
||||
<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
|
||||
</div>
|
||||
|
|
@ -537,7 +622,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<div style={{ marginBottom: 40 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Notification Settings</h3>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
||||
<VSCodeCheckbox
|
||||
checked={soundEnabled}
|
||||
onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
|
|
@ -564,12 +651,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="1"
|
||||
step="0.01"
|
||||
value={soundVolume ?? 0.5}
|
||||
onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}}
|
||||
onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
aria-label="Volume"
|
||||
/>
|
||||
<span style={{ minWidth: "35px", textAlign: "left" }}>
|
||||
|
|
@ -592,8 +675,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="60"
|
||||
step="1"
|
||||
value={rateLimitSeconds}
|
||||
onChange={(e) => setRateLimitSeconds(parseInt(e.target.value))}
|
||||
style={{ ...sliderStyle }}
|
||||
onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
|
||||
</div>
|
||||
|
|
@ -612,8 +695,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="5000"
|
||||
step="100"
|
||||
value={terminalOutputLineLimit ?? 500}
|
||||
onChange={(e) => setTerminalOutputLineLimit(parseInt(e.target.value))}
|
||||
style={{ ...sliderStyle }}
|
||||
onChange={(e) =>
|
||||
setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
|
||||
}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
|
||||
</div>
|
||||
|
|
@ -634,8 +719,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
max="500"
|
||||
step="1"
|
||||
value={maxOpenTabsContext ?? 20}
|
||||
onChange={(e) => setMaxOpenTabsContext(parseInt(e.target.value))}
|
||||
style={{ ...sliderStyle }}
|
||||
onChange={(e) =>
|
||||
setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))
|
||||
}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
|
||||
</div>
|
||||
|
|
@ -650,7 +737,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<VSCodeCheckbox
|
||||
checked={diffEnabled}
|
||||
onChange={(e: any) => {
|
||||
setDiffEnabled(e.target.checked)
|
||||
setCachedStateField("diffEnabled", e.target.checked)
|
||||
if (!e.target.checked) {
|
||||
// Reset experimental strategy when diffs are disabled
|
||||
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
|
||||
|
|
@ -689,11 +776,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
step="0.005"
|
||||
value={fuzzyMatchThreshold ?? 1.0}
|
||||
onChange={(e) => {
|
||||
setFuzzyMatchThreshold(parseFloat(e.target.value))
|
||||
}}
|
||||
style={{
|
||||
...sliderStyle,
|
||||
setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
|
||||
}}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
/>
|
||||
<span style={{ ...sliderLabelStyle }}>
|
||||
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
|
||||
|
|
@ -727,7 +812,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
<VSCodeCheckbox
|
||||
checked={checkpointsEnabled}
|
||||
onChange={(e: any) => {
|
||||
setCheckpointsEnabled(e.target.checked)
|
||||
setCachedStateField("checkpointsEnabled", e.target.checked)
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Enable experimental checkpoints</span>
|
||||
</VSCodeCheckbox>
|
||||
|
|
@ -783,7 +868,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
</VSCodeLink>
|
||||
</p>
|
||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
|
||||
v{version}
|
||||
v{extensionState.version}
|
||||
</p>
|
||||
|
||||
<p
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useDebounce } from "react-use"
|
||||
|
||||
interface TemperatureControlProps {
|
||||
value: number | undefined
|
||||
|
|
@ -9,13 +10,13 @@ interface TemperatureControlProps {
|
|||
|
||||
export const TemperatureControl = ({ value, onChange, maxValue = 1 }: TemperatureControlProps) => {
|
||||
const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined)
|
||||
const [inputValue, setInputValue] = useState(value?.toString() ?? "0")
|
||||
|
||||
const [inputValue, setInputValue] = useState(value)
|
||||
useDebounce(() => onChange(inputValue), 50, [onChange, inputValue])
|
||||
// Sync internal state with prop changes when switching profiles
|
||||
useEffect(() => {
|
||||
const hasCustomTemperature = value !== undefined
|
||||
setIsCustomTemperature(hasCustomTemperature)
|
||||
setInputValue(value?.toString() ?? "0")
|
||||
setInputValue(value)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
|
|
@ -26,9 +27,9 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
|
|||
const isChecked = e.target.checked
|
||||
setIsCustomTemperature(isChecked)
|
||||
if (!isChecked) {
|
||||
onChange(undefined) // Unset the temperature
|
||||
} else if (value !== undefined) {
|
||||
onChange(value) // Use the value from apiConfiguration, if set
|
||||
setInputValue(undefined) // Unset the temperature
|
||||
} else {
|
||||
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
|
||||
}
|
||||
}}>
|
||||
<span style={{ fontWeight: "500" }}>Use custom temperature</span>
|
||||
|
|
@ -48,27 +49,15 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
|
|||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<input
|
||||
aria-label="Temperature control text input"
|
||||
type="text"
|
||||
type="range"
|
||||
min="0"
|
||||
max={maxValue}
|
||||
step="0.01"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onBlur={(e) => {
|
||||
const newValue = parseFloat(e.target.value)
|
||||
if (!isNaN(newValue) && newValue >= 0 && newValue <= maxValue) {
|
||||
onChange(newValue)
|
||||
setInputValue(newValue.toString())
|
||||
} else {
|
||||
setInputValue(value?.toString() ?? "0") // Reset to last valid value
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: "60px",
|
||||
padding: "4px 8px",
|
||||
border: "1px solid var(--vscode-input-border)",
|
||||
background: "var(--vscode-input-background)",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
}}
|
||||
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
|
||||
onChange={(e) => setInputValue(parseFloat(e.target.value))}
|
||||
/>
|
||||
<span>{inputValue}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", marginTop: "8px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Higher values make output more random, lower values make it more deterministic.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,12 @@ describe("ApiOptions", () => {
|
|||
const renderApiOptions = (props = {}) => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<ApiOptions {...props} />
|
||||
<ApiOptions
|
||||
uriScheme={undefined}
|
||||
apiConfiguration={{}}
|
||||
setApiConfigurationField={() => {}}
|
||||
{...props}
|
||||
/>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import SettingsView from "../SettingsView"
|
||||
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
|
||||
|
|
@ -138,9 +137,9 @@ describe("SettingsView - Sound Settings", () => {
|
|||
fireEvent.click(soundCheckbox)
|
||||
expect(soundCheckbox).toBeChecked()
|
||||
|
||||
// Click Done to save settings
|
||||
const doneButton = screen.getByText("Done")
|
||||
fireEvent.click(doneButton)
|
||||
// Click Save to save settings
|
||||
const saveButton = screen.getByText("Save")
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -178,9 +177,9 @@ describe("SettingsView - Sound Settings", () => {
|
|||
const volumeSlider = screen.getByRole("slider", { name: /volume/i })
|
||||
fireEvent.change(volumeSlider, { target: { value: "0.75" } })
|
||||
|
||||
// Click Done to save settings
|
||||
const doneButton = screen.getByText("Done")
|
||||
fireEvent.click(doneButton)
|
||||
// Click Save to save settings
|
||||
const saveButton = screen.getByText("Save")
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
// Verify message sent to VSCode
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
|
|
@ -302,8 +301,8 @@ describe("SettingsView - Allowed Commands", () => {
|
|||
expect(commands).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("saves allowed commands when clicking Done", () => {
|
||||
const { onDone } = renderSettingsView()
|
||||
it("saves allowed commands when clicking Save", () => {
|
||||
renderSettingsView()
|
||||
|
||||
// Enable always allow execute
|
||||
const executeCheckbox = screen.getByRole("checkbox", {
|
||||
|
|
@ -317,9 +316,9 @@ describe("SettingsView - Allowed Commands", () => {
|
|||
const addButton = screen.getByText("Add")
|
||||
fireEvent.click(addButton)
|
||||
|
||||
// Click Done
|
||||
const doneButton = screen.getByText("Done")
|
||||
fireEvent.click(doneButton)
|
||||
// Click Save
|
||||
const saveButton = screen.getByText("Save")
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
// Verify VSCode messages were sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith(
|
||||
|
|
@ -328,6 +327,5 @@ describe("SettingsView - Allowed Commands", () => {
|
|||
commands: ["npm test"],
|
||||
}),
|
||||
)
|
||||
expect(onDone).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ describe("TemperatureControl", () => {
|
|||
const checkbox = screen.getByRole("checkbox")
|
||||
expect(checkbox).toBeChecked()
|
||||
|
||||
const input = screen.getByRole("textbox")
|
||||
const input = screen.getByRole("slider")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("0.7")
|
||||
})
|
||||
|
||||
it("updates when checkbox is toggled", () => {
|
||||
it("updates when checkbox is toggled", async () => {
|
||||
const onChange = jest.fn()
|
||||
render(<TemperatureControl value={0.7} onChange={onChange} />)
|
||||
|
||||
|
|
@ -31,40 +31,50 @@ describe("TemperatureControl", () => {
|
|||
|
||||
// Uncheck - should clear temperature
|
||||
fireEvent.click(checkbox)
|
||||
// Waiting for debounce
|
||||
await new Promise((x) => setTimeout(x, 100))
|
||||
expect(onChange).toHaveBeenCalledWith(undefined)
|
||||
|
||||
// Check - should restore previous temperature
|
||||
fireEvent.click(checkbox)
|
||||
// Waiting for debounce
|
||||
await new Promise((x) => setTimeout(x, 100))
|
||||
expect(onChange).toHaveBeenCalledWith(0.7)
|
||||
})
|
||||
|
||||
it("updates temperature when input loses focus", () => {
|
||||
it("updates temperature when input loses focus", async () => {
|
||||
const onChange = jest.fn()
|
||||
render(<TemperatureControl value={0.7} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole("textbox")
|
||||
const input = screen.getByRole("slider")
|
||||
fireEvent.change(input, { target: { value: "0.8" } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
// Waiting for debounce
|
||||
await new Promise((x) => setTimeout(x, 100))
|
||||
expect(onChange).toHaveBeenCalledWith(0.8)
|
||||
})
|
||||
|
||||
it("respects maxValue prop", () => {
|
||||
it("respects maxValue prop", async () => {
|
||||
const onChange = jest.fn()
|
||||
render(<TemperatureControl value={1.5} onChange={onChange} maxValue={2} />)
|
||||
|
||||
const input = screen.getByRole("textbox")
|
||||
const input = screen.getByRole("slider")
|
||||
|
||||
// Valid value within max
|
||||
fireEvent.change(input, { target: { value: "1.8" } })
|
||||
fireEvent.blur(input)
|
||||
// Waiting for debounce
|
||||
await new Promise((x) => setTimeout(x, 100))
|
||||
expect(onChange).toHaveBeenCalledWith(1.8)
|
||||
|
||||
// Invalid value above max
|
||||
fireEvent.change(input, { target: { value: "2.5" } })
|
||||
fireEvent.blur(input)
|
||||
expect(input).toHaveValue("1.5") // Should revert to original value
|
||||
expect(onChange).toHaveBeenCalledTimes(1) // Should not call onChange for invalid value
|
||||
expect(input).toHaveValue("2") // Clamped between 0 and 2
|
||||
// Waiting for debounce
|
||||
await new Promise((x) => setTimeout(x, 100))
|
||||
expect(onChange).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
it("syncs checkbox state when value prop changes", () => {
|
||||
|
|
|
|||
108
webview-ui/src/components/ui/alert-dialog.tsx
Normal file
108
webview-ui/src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-row justify-end space-x-2", className)} {...props} />
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-base text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "secondary" }), "mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
|
|
@ -5,19 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer active:opacity-90",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-primary-foreground bg-primary shadow hover:bg-primary/90",
|
||||
secondary: "text-secondary-foreground bg-secondary shadow-sm hover:bg-secondary/80",
|
||||
default:
|
||||
"border border-vscode-input-border bg-primary text-primary-foreground shadow hover:bg-primary/90 cursor-pointer",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 cursor-pointer",
|
||||
outline:
|
||||
"text-secondary-foreground bg-vscode-editor-background border border-vscode-dropdown-border shadow-sm hover:bg-vscode-editor-background/50",
|
||||
ghost: "text-secondary-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
destructive: "text-destructive-foreground bg-destructive shadow-sm hover:bg-destructive/90",
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
||||
secondary:
|
||||
"border border-vscode-input-border bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 cursor-pointer",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
||||
link: "text-primary underline-offset-4 hover:underline cursor-pointer",
|
||||
combobox:
|
||||
"text-secondary-foreground bg-vscode-input-background border border-vscode-input-border hover:bg-vscode-input-background/80",
|
||||
"text-vscode-font-size font-normal text-popover-foreground bg-vscode-input-background border border-vscode-dropdown-border hover:bg-vscode-input-background/80 cursor-pointer",
|
||||
},
|
||||
size: {
|
||||
default: "h-7 px-3",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const CommandInput = React.forwardRef<
|
|||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-base outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -108,7 +108,7 @@ const CommandItem = React.forwardRef<
|
|||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm text-vscode-dropdown-foreground outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"relative flex cursor-pointer gap-2 select-none items-center rounded-sm px-2 py-1.5 text-base text-vscode-dropdown-foreground outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -38,12 +38,12 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<DialogPrimitive.Close className="cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex w-full text-vscode-input-foreground border border-vscode-input-border bg-vscode-input-background rounded-xs px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useState } from "react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { validateApiConfiguration } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "../settings/ApiOptions"
|
||||
|
||||
const WelcomeView = () => {
|
||||
const { apiConfiguration, currentApiConfigName } = useExtensionState()
|
||||
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = useCallback(() => {
|
||||
const error = validateApiConfiguration(apiConfiguration)
|
||||
if (error) {
|
||||
setErrorMessage(error)
|
||||
|
|
@ -22,7 +22,7 @@ const WelcomeView = () => {
|
|||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
}
|
||||
}, [apiConfiguration, currentApiConfigName])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen px-0 pb-5">
|
||||
|
|
@ -37,10 +37,15 @@ const WelcomeView = () => {
|
|||
<b>To get started, this extension needs an API provider.</b>
|
||||
|
||||
<div className="mt-3">
|
||||
<ApiOptions fromWelcomeView />
|
||||
<ApiOptions
|
||||
fromWelcomeView
|
||||
apiConfiguration={apiConfiguration || {}}
|
||||
uriScheme={uriScheme}
|
||||
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 bg-[var(--vscode-editor-background)] py-3">
|
||||
<div className="sticky bottom-0 bg-[var(--vscode-sideBar-background)] py-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<VSCodeButton onClick={handleSubmit}>Let's go!</VSCodeButton>
|
||||
{errorMessage && <span className="text-destructive">{errorMessage}</span>}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setEnhancementApiConfigId: (value: string) => void
|
||||
setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
|
||||
setAutoApprovalEnabled: (value: boolean) => void
|
||||
handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
|
||||
customModes: ModeConfig[]
|
||||
setCustomModes: (value: ModeConfig[]) => void
|
||||
setMaxOpenTabsContext: (value: number) => void
|
||||
|
|
@ -159,21 +158,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
})
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
// Returns a function that handles an input change event for a specific API configuration field.
|
||||
// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
|
||||
(field: keyof ApiConfiguration) => (event: any) => {
|
||||
// Use the functional form of setState to ensure the latest state is used in the update logic.
|
||||
setState((currentState) => {
|
||||
return {
|
||||
...currentState,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
|
@ -298,7 +282,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value,
|
||||
apiConfiguration: {
|
||||
...prevState.apiConfiguration,
|
||||
...value,
|
||||
},
|
||||
})),
|
||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
||||
|
|
@ -336,7 +323,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setEnhancementApiConfigId: (value) =>
|
||||
setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||
handleInputChange,
|
||||
setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
|
||||
setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue