Prettier backfill

This commit is contained in:
Matt Rubens
2025-01-17 14:11:28 -05:00
parent 3bcb4ff8c5
commit 60a0a824b9
174 changed files with 15715 additions and 15428 deletions

View File

@@ -3,223 +3,216 @@ import { memo, useEffect, useRef, useState } from "react"
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
interface ApiConfigManagerProps {
currentApiConfigName?: string
listApiConfigMeta?: ApiConfigMeta[]
onSelectConfig: (configName: string) => void
onDeleteConfig: (configName: string) => void
onRenameConfig: (oldName: string, newName: string) => void
onUpsertConfig: (configName: string) => void
currentApiConfigName?: string
listApiConfigMeta?: ApiConfigMeta[]
onSelectConfig: (configName: string) => void
onDeleteConfig: (configName: string) => void
onRenameConfig: (oldName: string, newName: string) => void
onUpsertConfig: (configName: string) => void
}
const ApiConfigManager = ({
currentApiConfigName = "",
listApiConfigMeta = [],
onSelectConfig,
onDeleteConfig,
onRenameConfig,
onUpsertConfig,
currentApiConfigName = "",
listApiConfigMeta = [],
onSelectConfig,
onDeleteConfig,
onRenameConfig,
onUpsertConfig,
}: ApiConfigManagerProps) => {
const [editState, setEditState] = useState<'new' | 'rename' | null>(null);
const [inputValue, setInputValue] = useState("");
const inputRef = useRef<HTMLInputElement>();
const [editState, setEditState] = useState<"new" | "rename" | null>(null)
const [inputValue, setInputValue] = useState("")
const inputRef = useRef<HTMLInputElement>()
// Focus input when entering edit mode
useEffect(() => {
if (editState) {
setTimeout(() => inputRef.current?.focus(), 0);
}
}, [editState]);
// Focus input when entering edit mode
useEffect(() => {
if (editState) {
setTimeout(() => inputRef.current?.focus(), 0)
}
}, [editState])
// Reset edit state when current profile changes
useEffect(() => {
setEditState(null);
setInputValue("");
}, [currentApiConfigName]);
// Reset edit state when current profile changes
useEffect(() => {
setEditState(null)
setInputValue("")
}, [currentApiConfigName])
const handleAdd = () => {
const newConfigName = currentApiConfigName + " (copy)";
onUpsertConfig(newConfigName);
};
const handleAdd = () => {
const newConfigName = currentApiConfigName + " (copy)"
onUpsertConfig(newConfigName)
}
const handleStartRename = () => {
setEditState('rename');
setInputValue(currentApiConfigName || "");
};
const handleStartRename = () => {
setEditState("rename")
setInputValue(currentApiConfigName || "")
}
const handleCancel = () => {
setEditState(null);
setInputValue("");
};
const handleCancel = () => {
setEditState(null)
setInputValue("")
}
const handleSave = () => {
const trimmedValue = inputValue.trim();
if (!trimmedValue) return;
const handleSave = () => {
const trimmedValue = inputValue.trim()
if (!trimmedValue) return
if (editState === 'new') {
onUpsertConfig(trimmedValue);
} else if (editState === 'rename' && currentApiConfigName) {
onRenameConfig(currentApiConfigName, trimmedValue);
}
if (editState === "new") {
onUpsertConfig(trimmedValue)
} else if (editState === "rename" && currentApiConfigName) {
onRenameConfig(currentApiConfigName, trimmedValue)
}
setEditState(null);
setInputValue("");
};
setEditState(null)
setInputValue("")
}
const handleDelete = () => {
if (!currentApiConfigName || !listApiConfigMeta || listApiConfigMeta.length <= 1) return;
// Let the extension handle both deletion and selection
onDeleteConfig(currentApiConfigName);
};
const handleDelete = () => {
if (!currentApiConfigName || !listApiConfigMeta || listApiConfigMeta.length <= 1) return
const isOnlyProfile = listApiConfigMeta?.length === 1;
// Let the extension handle both deletion and selection
onDeleteConfig(currentApiConfigName)
}
return (
<div style={{ marginBottom: 5 }}>
<div style={{
display: "flex",
flexDirection: "column",
gap: "2px"
}}>
<label htmlFor="config-profile">
<span style={{ fontWeight: "500" }}>Configuration Profile</span>
</label>
const isOnlyProfile = listApiConfigMeta?.length === 1
{editState ? (
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
<VSCodeTextField
ref={inputRef as any}
value={inputValue}
onInput={(e: any) => setInputValue(e.target.value)}
placeholder={editState === 'new' ? "Enter profile name" : "Enter new name"}
style={{ flexGrow: 1 }}
onKeyDown={(e: any) => {
if (e.key === 'Enter' && inputValue.trim()) {
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
}}
/>
<VSCodeButton
appearance="icon"
disabled={!inputValue.trim()}
onClick={handleSave}
title="Save"
style={{
padding: 0,
margin: 0,
height: '28px',
width: '28px',
minWidth: '28px'
}}
>
<span className="codicon codicon-check" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
onClick={handleCancel}
title="Cancel"
style={{
padding: 0,
margin: 0,
height: '28px',
width: '28px',
minWidth: '28px'
}}
>
<span className="codicon codicon-close" />
</VSCodeButton>
</div>
) : (
<>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
<select
id="config-profile"
value={currentApiConfigName}
onChange={(e) => onSelectConfig(e.target.value)}
style={{
flexGrow: 1,
padding: "4px 8px",
paddingRight: "24px",
backgroundColor: "var(--vscode-dropdown-background)",
color: "var(--vscode-dropdown-foreground)",
border: "1px solid var(--vscode-dropdown-border)",
borderRadius: "2px",
height: "28px",
cursor: "pointer",
outline: "none"
}}
>
{listApiConfigMeta?.map((config) => (
<option
key={config.name}
value={config.name}
>
{config.name}
</option>
))}
</select>
<VSCodeButton
appearance="icon"
onClick={handleAdd}
title="Add profile"
style={{
padding: 0,
margin: 0,
height: '28px',
width: '28px',
minWidth: '28px'
}}
>
<span className="codicon codicon-add" />
</VSCodeButton>
{currentApiConfigName && (
<>
<VSCodeButton
appearance="icon"
onClick={handleStartRename}
title="Rename profile"
style={{
padding: 0,
margin: 0,
height: '28px',
width: '28px',
minWidth: '28px'
}}
>
<span className="codicon codicon-edit" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
onClick={handleDelete}
title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
disabled={isOnlyProfile}
style={{
padding: 0,
margin: 0,
height: '28px',
width: '28px',
minWidth: '28px'
}}
>
<span className="codicon codicon-trash" />
</VSCodeButton>
</>
)}
</div>
<p style={{
fontSize: "12px",
margin: "5px 0 12px",
color: "var(--vscode-descriptionForeground)"
}}>
Save different API configurations to quickly switch between providers and settings
</p>
</>
)}
</div>
</div>
)
return (
<div style={{ marginBottom: 5 }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2px",
}}>
<label htmlFor="config-profile">
<span style={{ fontWeight: "500" }}>Configuration Profile</span>
</label>
{editState ? (
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
<VSCodeTextField
ref={inputRef as any}
value={inputValue}
onInput={(e: any) => setInputValue(e.target.value)}
placeholder={editState === "new" ? "Enter profile name" : "Enter new name"}
style={{ flexGrow: 1 }}
onKeyDown={(e: any) => {
if (e.key === "Enter" && inputValue.trim()) {
handleSave()
} else if (e.key === "Escape") {
handleCancel()
}
}}
/>
<VSCodeButton
appearance="icon"
disabled={!inputValue.trim()}
onClick={handleSave}
title="Save"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
<span className="codicon codicon-check" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
onClick={handleCancel}
title="Cancel"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
<span className="codicon codicon-close" />
</VSCodeButton>
</div>
) : (
<>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
<select
id="config-profile"
value={currentApiConfigName}
onChange={(e) => onSelectConfig(e.target.value)}
style={{
flexGrow: 1,
padding: "4px 8px",
paddingRight: "24px",
backgroundColor: "var(--vscode-dropdown-background)",
color: "var(--vscode-dropdown-foreground)",
border: "1px solid var(--vscode-dropdown-border)",
borderRadius: "2px",
height: "28px",
cursor: "pointer",
outline: "none",
}}>
{listApiConfigMeta?.map((config) => (
<option key={config.name} value={config.name}>
{config.name}
</option>
))}
</select>
<VSCodeButton
appearance="icon"
onClick={handleAdd}
title="Add profile"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
<span className="codicon codicon-add" />
</VSCodeButton>
{currentApiConfigName && (
<>
<VSCodeButton
appearance="icon"
onClick={handleStartRename}
title="Rename profile"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
<span className="codicon codicon-edit" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
onClick={handleDelete}
title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
disabled={isOnlyProfile}
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
<span className="codicon codicon-trash" />
</VSCodeButton>
</>
)}
</div>
<p
style={{
fontSize: "12px",
margin: "5px 0 12px",
color: "var(--vscode-descriptionForeground)",
}}>
Save different API configurations to quickly switch between providers and settings
</p>
</>
)}
</div>
</div>
)
}
export default memo(ApiConfigManager)
export default memo(ApiConfigManager)

View File

@@ -1,11 +1,6 @@
import { Checkbox, Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import {
VSCodeLink,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField
} from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
import {
@@ -83,7 +78,12 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
requestLocalModels()
}
}, [selectedProvider, requestLocalModels])
useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm" ? 2000 : null)
useInterval(
requestLocalModels,
selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm"
? 2000
: null,
)
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "ollamaModels" && message.ollamaModels) {
@@ -102,17 +102,19 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
...Object.keys(models).map((modelId) => ({
value: modelId,
label: modelId,
}))
})),
]
return (
<Dropdown
id="model-id"
value={selectedModelId}
onChange={(value: unknown) => {handleInputChange("apiModelId")({
target: {
value: (value as DropdownOption).value
}
})}}
onChange={(value: unknown) => {
handleInputChange("apiModelId")({
target: {
value: (value as DropdownOption).value,
},
})
}}
style={{ width: "100%" }}
options={options}
/>
@@ -131,8 +133,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
onChange={(value: unknown) => {
handleInputChange("apiProvider")({
target: {
value: (value as DropdownOption).value
}
value: (value as DropdownOption).value,
},
})
}}
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
@@ -149,7 +151,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ value: "vscode-lm", label: "VS Code LM API" },
{ value: "mistral", label: "Mistral" },
{ value: "lmstudio", label: "LM Studio" },
{ value: "ollama", label: "Ollama" }
{ value: "ollama", label: "Ollama" },
]}
/>
</div>
@@ -331,7 +333,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
target: { value: checked },
})
}}>
Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
Compress prompts and message chains to the context size (
<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
</Checkbox>
<br />
</div>
@@ -371,11 +374,13 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={(value: unknown) => {handleInputChange("awsRegion")({
target: {
value: (value as DropdownOption).value
}
})}}
onChange={(value: unknown) => {
handleInputChange("awsRegion")({
target: {
value: (value as DropdownOption).value,
},
})
}}
options={[
{ value: "", label: "Select a region..." },
{ value: "us-east-1", label: "us-east-1" },
@@ -392,7 +397,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ value: "eu-west-2", label: "eu-west-2" },
{ value: "eu-west-3", label: "eu-west-3" },
{ value: "sa-east-1", label: "sa-east-1" },
{ value: "us-gov-west-1", label: "us-gov-west-1" }
{ value: "us-gov-west-1", label: "us-gov-west-1" },
]}
/>
</div>
@@ -435,18 +440,20 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }}
onChange={(value: unknown) => {handleInputChange("vertexRegion")({
target: {
value: (value as DropdownOption).value
}
})}}
onChange={(value: unknown) => {
handleInputChange("vertexRegion")({
target: {
value: (value as DropdownOption).value,
},
})
}}
options={[
{ value: "", label: "Select a region..." },
{ value: "us-east5", label: "us-east5" },
{ value: "us-central1", label: "us-central1" },
{ value: "europe-west1", label: "europe-west1" },
{ value: "europe-west4", label: "europe-west4" },
{ value: "asia-southeast1", label: "asia-southeast1" }
{ value: "asia-southeast1", label: "asia-southeast1" },
]}
/>
</div>
@@ -520,7 +527,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<OpenAiModelPicker />
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: "flex", alignItems: "center" }}>
<Checkbox
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
onChange={(checked: boolean) => {
@@ -669,19 +676,21 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{vsCodeLmModels.length > 0 ? (
<Dropdown
id="vscode-lm-model"
value={apiConfiguration?.vsCodeLmModelSelector ?
`${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` :
""}
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onChange={(value: unknown) => {
const valueStr = (value as DropdownOption)?.value;
const valueStr = (value as DropdownOption)?.value
if (!valueStr) {
return
}
const [vendor, family] = valueStr.split('/');
const [vendor, family] = valueStr.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family }
}
value: { vendor, family },
},
})
}}
style={{ width: "100%" }}
@@ -689,18 +698,20 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ value: "", label: "Select a model..." },
...vsCodeLmModels.map((model) => ({
value: `${model.vendor}/${model.family}`,
label: `${model.vendor} - ${model.family}`
}))
label: `${model.vendor} - ${model.family}`,
})),
]}
/>
) : (
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
The VS Code Language Model API allows you to run models provided by other VS Code extensions (including but not limited to GitHub Copilot).
The easiest way to get started is to install the Copilot and Copilot Chat extensions from the VS Code Marketplace.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
The VS Code Language Model API allows you to run models provided by other VS Code
extensions (including but not limited to GitHub Copilot). The easiest way to get started
is to install the Copilot and Copilot Chat extensions from the VS Code Marketplace.
</p>
)}
@@ -711,7 +722,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
color: "var(--vscode-errorForeground)",
fontWeight: 500,
}}>
Note: This is a very experimental integration and may not work as expected. Please report any issues to the Roo-Cline GitHub repository.
Note: This is a very experimental integration and may not work as expected. Please report
any issues to the Roo-Cline GitHub repository.
</p>
</div>
</div>
@@ -1042,9 +1054,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
case "vscode-lm":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.vsCodeLmModelSelector ?
`${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}` :
"",
selectedModelId: apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
supportsImages: false, // VSCode LM API currently doesn't support images

View File

@@ -38,7 +38,6 @@ const GlamaModelPicker: React.FC = () => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
useEffect(() => {
if (apiConfiguration?.glamaModelId && apiConfiguration?.glamaModelId !== searchTerm) {
setSearchTerm(apiConfiguration?.glamaModelId)
@@ -47,18 +46,15 @@ const GlamaModelPicker: React.FC = () => {
const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshGlamaModels" })
},
50
),
[]
debounce(() => {
vscode.postMessage({ type: "refreshGlamaModels" })
}, 50),
[],
)
useMount(() => {
debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
@@ -91,7 +87,7 @@ const GlamaModelPicker: React.FC = () => {
const fzf = useMemo(() => {
return new Fzf(searchableItems, {
selector: item => item.html
selector: (item) => item.html,
})
}, [searchableItems])
@@ -99,9 +95,9 @@ const GlamaModelPicker: React.FC = () => {
if (!searchTerm) return searchableItems
const searchResults = fzf.find(searchTerm)
return searchResults.map(result => ({
return searchResults.map((result) => ({
...result.item,
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
}))
}, [searchableItems, searchTerm, fzf])

View File

@@ -37,19 +37,16 @@ const OpenAiModelPicker: React.FC = () => {
const debouncedRefreshModels = useMemo(
() =>
debounce(
(baseUrl: string, apiKey: string) => {
vscode.postMessage({
type: "refreshOpenAiModels",
values: {
baseUrl,
apiKey
}
})
},
50
),
[]
debounce((baseUrl: string, apiKey: string) => {
vscode.postMessage({
type: "refreshOpenAiModels",
values: {
baseUrl,
apiKey,
},
})
}, 50),
[],
)
useEffect(() => {
@@ -57,10 +54,7 @@ const OpenAiModelPicker: React.FC = () => {
return
}
debouncedRefreshModels(
apiConfiguration.openAiBaseUrl,
apiConfiguration.openAiApiKey
)
debouncedRefreshModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
// Cleanup debounced function
return () => {
@@ -94,7 +88,7 @@ const OpenAiModelPicker: React.FC = () => {
const fzf = useMemo(() => {
return new Fzf(searchableItems, {
selector: item => item.html
selector: (item) => item.html,
})
}, [searchableItems])
@@ -102,9 +96,9 @@ const OpenAiModelPicker: React.FC = () => {
if (!searchTerm) return searchableItems
const searchResults = fzf.find(searchTerm)
return searchResults.map(result => ({
return searchResults.map((result) => ({
...result.item,
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
}))
}, [searchableItems, searchTerm, fzf])

View File

@@ -46,18 +46,15 @@ const OpenRouterModelPicker: React.FC = () => {
const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshOpenRouterModels" })
},
50
),
[]
debounce(() => {
vscode.postMessage({ type: "refreshOpenRouterModels" })
}, 50),
[],
)
useMount(() => {
debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
@@ -90,7 +87,7 @@ const OpenRouterModelPicker: React.FC = () => {
const fzf = useMemo(() => {
return new Fzf(searchableItems, {
selector: item => item.html
selector: (item) => item.html,
})
}, [searchableItems])
@@ -98,9 +95,9 @@ const OpenRouterModelPicker: React.FC = () => {
if (!searchTerm) return searchableItems
const searchResults = fzf.find(searchTerm)
return searchResults.map(result => ({
return searchResults.map((result) => ({
...result.item,
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight")
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
}))
}, [searchableItems, searchTerm, fzf])

View File

@@ -1,4 +1,10 @@
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import {
VSCodeButton,
VSCodeCheckbox,
VSCodeLink,
VSCodeTextArea,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
@@ -61,7 +67,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
listApiConfigMeta,
mode,
setMode,
experimentalDiffStrategy,
experimentalDiffStrategy,
setExperimentalDiffStrategy,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
@@ -77,7 +83,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration
apiConfiguration,
})
vscode.postMessage({ type: "customInstructions", text: customInstructions })
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
@@ -102,10 +108,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({
type: "upsertApiConfiguration",
text: currentApiConfigName,
apiConfiguration
apiConfiguration,
})
vscode.postMessage({ type: "mode", text: mode })
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
onDone()
}
}
@@ -135,7 +141,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setCommandInput("")
vscode.postMessage({
type: "allowedCommands",
commands: newCommands
commands: newCommands,
})
}
}
@@ -161,53 +167,53 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginBottom: "17px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
</div>
<div
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
<div style={{ marginBottom: 5 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
Provider Settings
</h3>
<ApiConfigManager
currentApiConfigName={currentApiConfigName}
listApiConfigMeta={listApiConfigMeta}
onSelectConfig={(configName: string) => {
vscode.postMessage({
type: "loadApiConfiguration",
text: configName
text: configName,
})
}}
onDeleteConfig={(configName: string) => {
vscode.postMessage({
type: "deleteApiConfiguration",
text: configName
text: configName,
})
}}
onRenameConfig={(oldName: string, newName: string) => {
vscode.postMessage({
type: "renameApiConfiguration",
values: { oldName, newName },
apiConfiguration
apiConfiguration,
})
}}
onUpsertConfig={(configName: string) => {
vscode.postMessage({
type: "upsertApiConfiguration",
text: configName,
apiConfiguration
apiConfiguration,
})
}}
/>
<ApiOptions
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
<ApiOptions apiErrorMessage={apiErrorMessage} modelIdErrorMessage={modelIdErrorMessage} />
</div>
<div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Agent Settings</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
Agent Settings
</h3>
<div style={{ marginBottom: 15 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Agent Mode</label>
@@ -225,22 +231,27 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
height: "28px"
height: "28px",
}}>
<option value="code">Code</option>
<option value="architect">Architect</option>
<option value="ask">Ask</option>
</select>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the mode that best fits your needs. Code mode focuses on implementation details, Architect mode on high-level design, and Ask mode on asking questions about the codebase.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the mode that best fits your needs. Code mode focuses on implementation details,
Architect mode on high-level design, and Ask mode on asking questions about the
codebase.
</p>
</div>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Preferred Language
</label>
<select
value={preferredLanguage}
onChange={(e) => setPreferredLanguage(e.target.value)}
@@ -251,7 +262,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
height: "28px"
height: "28px",
}}>
<option value="English">English</option>
<option value="Arabic">Arabic - العربية</option>
@@ -272,11 +283,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<option value="Traditional Chinese">Traditional Chinese - </option>
<option value="Turkish">Turkish - Türkçe</option>
</select>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the language that Cline should use for communication.
</p>
</div>
@@ -298,7 +310,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules in the working directory are also included. For mode-specific instructions, use the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> Prompts tab in the top menu.
These instructions are added to the end of the system prompt sent with every request. Custom
instructions set in .clinerules in the working directory are also included. For
mode-specific instructions, use the{" "}
<span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> Prompts tab
in the top menu.
</p>
</div>
@@ -306,8 +322,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</div>
<div style={{ marginBottom: 5 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '150px' }}>Terminal output limit</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ fontWeight: "500", minWidth: "150px" }}>Terminal output limit</span>
<input
type="range"
min="100"
@@ -317,27 +333,28 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => setTerminalOutputLineLimit(parseInt(e.target.value))}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
/>
<span style={{ minWidth: '45px', textAlign: 'left' }}>
{terminalOutputLineLimit ?? 500}
</span>
<span style={{ minWidth: "45px", textAlign: "left" }}>{terminalOutputLineLimit ?? 500}</span>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Maximum number of lines to include in terminal output when executing commands. When exceeded lines will be removed from the middle, saving tokens.
Maximum number of lines to include in terminal output when executing commands. When exceeded
lines will be removed from the middle, saving tokens.
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => {
setDiffEnabled(e.target.checked)
if (!e.target.checked) {
// Reset experimental strategy when diffs are disabled
setExperimentalDiffStrategy(false)
}
}}>
<VSCodeCheckbox
checked={diffEnabled}
onChange={(e: any) => {
setDiffEnabled(e.target.checked)
if (!e.target.checked) {
// Reset experimental strategy when diffs are disabled
setExperimentalDiffStrategy(false)
}
}}>
<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
</VSCodeCheckbox>
<p
@@ -346,12 +363,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
When enabled, Cline will be able to edit files more quickly and will automatically reject
truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
</p>
{diffEnabled && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ color: "var(--vscode-errorForeground)" }}></span>
<VSCodeCheckbox
checked={experimentalDiffStrategy}
@@ -359,13 +377,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<span style={{ fontWeight: "500" }}>Use experimental unified diff strategy</span>
</VSCodeCheckbox>
</div>
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits.
<p
style={{
fontSize: "12px",
marginBottom: 15,
color: "var(--vscode-descriptionForeground)",
}}>
Enable the experimental unified diff strategy. This strategy might reduce the number of
retries caused by model errors but may cause unexpected behavior or incorrect edits.
Only enable if you understand the risks and are willing to carefully review all changes.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
<input
type="range"
min="0.8"
@@ -373,20 +397,27 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
step="0.005"
value={fuzzyMatchThreshold ?? 1.0}
onChange={(e) => {
setFuzzyMatchThreshold(parseFloat(e.target.value));
setFuzzyMatchThreshold(parseFloat(e.target.value))
}}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
<span style={{ minWidth: "35px", textAlign: "left" }}>
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
</span>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
This slider controls how precisely code sections must match when applying diffs. Lower
values allow more flexible matching but increase the risk of incorrect replacements. Use
values below 100% with extreme caution.
</p>
</div>
)}
@@ -409,11 +440,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 15, border: "2px solid var(--vscode-errorForeground)", borderRadius: "4px", padding: "10px" }}>
<h4 style={{ fontWeight: 500, margin: "0 0 10px 0", color: "var(--vscode-errorForeground)" }}> High-Risk Auto-Approve Settings</h4>
<div
style={{
marginBottom: 15,
border: "2px solid var(--vscode-errorForeground)",
borderRadius: "4px",
padding: "10px",
}}>
<h4 style={{ fontWeight: 500, margin: "0 0 10px 0", color: "var(--vscode-errorForeground)" }}>
High-Risk Auto-Approve Settings
</h4>
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
The following settings allow Cline to automatically perform potentially dangerous operations without requiring approval.
Enable these settings only if you fully trust the AI and understand the associated security risks.
The following settings allow Cline to automatically perform potentially dangerous operations
without requiring approval. Enable these settings only if you fully trust the AI and understand
the associated security risks.
</p>
<div style={{ marginBottom: 5 }}>
@@ -427,7 +467,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
{alwaysAllowWrite && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<input
type="range"
min="0"
@@ -437,15 +477,18 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => setWriteDelayMs(parseInt(e.target.value))}
style={{
flex: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
/>
<span style={{ minWidth: '45px', textAlign: 'left' }}>
{writeDelayMs}ms
</span>
<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Delay after writes to allow diagnostics to detect potential problems
</p>
</div>
@@ -459,7 +502,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically perform browser actions without requiring approval<br />
Automatically perform browser actions without requiring approval
<br />
Note: Only applies when the model supports computer use
</p>
</div>
@@ -475,7 +519,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
{alwaysApproveResubmit && (
<div style={{ marginTop: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<input
type="range"
min="0"
@@ -485,15 +529,18 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => setRequestDelaySeconds(parseInt(e.target.value))}
style={{
flex: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
/>
<span style={{ minWidth: '45px', textAlign: 'left' }}>
{requestDelaySeconds}s
</span>
<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Delay before retrying the request
</p>
</div>
@@ -507,7 +554,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this setting and the tool's individual "Always allow" checkbox)
Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this
setting and the tool's individual "Always allow" checkbox)
</p>
</div>
@@ -524,20 +572,22 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
{alwaysAllowExecute && (
<div style={{ marginTop: 10 }}>
<span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Command prefixes that can be auto-executed when "Always approve execute operations" is enabled.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Command prefixes that can be auto-executed when "Always approve execute operations"
is enabled.
</p>
<div style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
<VSCodeTextField
value={commandInput}
onInput={(e: any) => setCommandInput(e.target.value)}
onKeyDown={(e: any) => {
if (e.key === 'Enter') {
if (e.key === "Enter") {
e.preventDefault()
handleAddCommand()
}
@@ -545,51 +595,53 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
placeholder="Enter command prefix (e.g., 'git ')"
style={{ flexGrow: 1 }}
/>
<VSCodeButton onClick={handleAddCommand}>
Add
</VSCodeButton>
<VSCodeButton onClick={handleAddCommand}>Add</VSCodeButton>
</div>
<div style={{
marginTop: '10px',
display: 'flex',
flexWrap: 'wrap',
gap: '5px'
}}>
<div
style={{
marginTop: "10px",
display: "flex",
flexWrap: "wrap",
gap: "5px",
}}>
{(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'
}}>
<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",
}}>
<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)',
height: "20px",
width: "20px",
minWidth: "20px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--vscode-button-foreground)",
}}
onClick={() => {
const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
const newCommands = (allowedCommands ?? []).filter(
(_, i) => i !== index,
)
setAllowedCommands(newCommands)
vscode.postMessage({
type: "allowedCommands",
commands: newCommands
commands: newCommands,
})
}}
>
}}>
<span className="codicon codicon-close" />
</VSCodeButton>
</div>
@@ -603,8 +655,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 10 }}>
<div style={{ marginBottom: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Browser Settings</h3>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
Browser Settings
</h3>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Viewport size
</label>
<select
value={browserViewportSize}
onChange={(e) => setBrowserViewportSize(e.target.value)}
@@ -615,25 +671,27 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
height: "28px"
height: "28px",
}}>
<option value="1280x800">Large Desktop (1280x800)</option>
<option value="900x600">Small Desktop (900x600)</option>
<option value="768x1024">Tablet (768x1024)</option>
<option value="360x640">Mobile (360x640)</option>
</select>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the viewport size for browser interactions. This affects how websites are displayed and interacted with.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the viewport size for browser interactions. This affects how websites are
displayed and interacted with.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Screenshot quality</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ fontWeight: "500", minWidth: "100px" }}>Screenshot quality</span>
<input
type="range"
min="1"
@@ -643,28 +701,32 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
{screenshotQuality ?? 75}%
</span>
<span style={{ minWidth: "35px", textAlign: "left" }}>{screenshotQuality ?? 75}%</span>
</div>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Adjust the WebP quality of browser screenshots. Higher values provide clearer screenshots but increase token usage.
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Adjust the WebP quality of browser screenshots. Higher values provide clearer
screenshots but increase token usage.
</p>
</div>
</div>
<div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 10 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
Notification Settings
</h3>
<VSCodeCheckbox
checked={soundEnabled}
onChange={(e: any) => setSoundEnabled(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
</VSCodeCheckbox>
<p
@@ -678,8 +740,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</div>
{soundEnabled && (
<div style={{ marginLeft: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ fontWeight: "500", minWidth: '100px' }}>Volume</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ fontWeight: "500", minWidth: "100px" }}>Volume</span>
<input
type="range"
min="0"
@@ -689,12 +751,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
style={{
flexGrow: 1,
accentColor: 'var(--vscode-button-background)',
height: '2px'
accentColor: "var(--vscode-button-background)",
height: "2px",
}}
aria-label="Volume"
/>
<span style={{ minWidth: '35px', textAlign: 'left' }}>
<span style={{ minWidth: "35px", textAlign: "left" }}>
{((soundVolume ?? 0.5) * 100).toFixed(0)}%
</span>
</div>
@@ -733,7 +795,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
If you have any questions or feedback, feel free to open an issue at{" "}
<VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
github.com/RooVetGit/Roo-Cline
</VSCodeLink> or join {" "}
</VSCodeLink>{" "}
or join{" "}
<VSCodeLink href="https://www.reddit.com/r/roocline/" style={{ display: "inline" }}>
reddit.com/r/roocline
</VSCodeLink>

View File

@@ -1,154 +1,136 @@
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ApiConfigManager from '../ApiConfigManager';
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import ApiConfigManager from "../ApiConfigManager"
// Mock VSCode components
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
VSCodeButton: ({ children, onClick, title, disabled }: any) => (
<button onClick={onClick} title={title} disabled={disabled}>
{children}
</button>
),
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
<input
value={value}
onChange={e => onInput(e)}
placeholder={placeholder}
ref={undefined} // Explicitly set ref to undefined to avoid warning
/>
),
}));
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeButton: ({ children, onClick, title, disabled }: any) => (
<button onClick={onClick} title={title} disabled={disabled}>
{children}
</button>
),
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
<input
value={value}
onChange={(e) => onInput(e)}
placeholder={placeholder}
ref={undefined} // Explicitly set ref to undefined to avoid warning
/>
),
}))
describe('ApiConfigManager', () => {
const mockOnSelectConfig = jest.fn();
const mockOnDeleteConfig = jest.fn();
const mockOnRenameConfig = jest.fn();
const mockOnUpsertConfig = jest.fn();
describe("ApiConfigManager", () => {
const mockOnSelectConfig = jest.fn()
const mockOnDeleteConfig = jest.fn()
const mockOnRenameConfig = jest.fn()
const mockOnUpsertConfig = jest.fn()
const defaultProps = {
currentApiConfigName: 'Default Config',
listApiConfigMeta: [
{ name: 'Default Config' },
{ name: 'Another Config' }
],
onSelectConfig: mockOnSelectConfig,
onDeleteConfig: mockOnDeleteConfig,
onRenameConfig: mockOnRenameConfig,
onUpsertConfig: mockOnUpsertConfig,
};
const defaultProps = {
currentApiConfigName: "Default Config",
listApiConfigMeta: [{ name: "Default Config" }, { name: "Another Config" }],
onSelectConfig: mockOnSelectConfig,
onDeleteConfig: mockOnDeleteConfig,
onRenameConfig: mockOnRenameConfig,
onUpsertConfig: mockOnUpsertConfig,
}
beforeEach(() => {
jest.clearAllMocks();
});
beforeEach(() => {
jest.clearAllMocks()
})
it('immediately creates a copy when clicking add button', () => {
render(<ApiConfigManager {...defaultProps} />);
it("immediately creates a copy when clicking add button", () => {
render(<ApiConfigManager {...defaultProps} />)
// Find and click the add button
const addButton = screen.getByTitle('Add profile');
fireEvent.click(addButton);
// Find and click the add button
const addButton = screen.getByTitle("Add profile")
fireEvent.click(addButton)
// Verify that onUpsertConfig was called with the correct name
expect(mockOnUpsertConfig).toHaveBeenCalledTimes(1);
expect(mockOnUpsertConfig).toHaveBeenCalledWith('Default Config (copy)');
});
// Verify that onUpsertConfig was called with the correct name
expect(mockOnUpsertConfig).toHaveBeenCalledTimes(1)
expect(mockOnUpsertConfig).toHaveBeenCalledWith("Default Config (copy)")
})
it('creates copy with correct name when current config has spaces', () => {
render(
<ApiConfigManager
{...defaultProps}
currentApiConfigName="My Test Config"
/>
);
it("creates copy with correct name when current config has spaces", () => {
render(<ApiConfigManager {...defaultProps} currentApiConfigName="My Test Config" />)
const addButton = screen.getByTitle('Add profile');
fireEvent.click(addButton);
const addButton = screen.getByTitle("Add profile")
fireEvent.click(addButton)
expect(mockOnUpsertConfig).toHaveBeenCalledWith('My Test Config (copy)');
});
expect(mockOnUpsertConfig).toHaveBeenCalledWith("My Test Config (copy)")
})
it('handles empty current config name gracefully', () => {
render(
<ApiConfigManager
{...defaultProps}
currentApiConfigName=""
/>
);
it("handles empty current config name gracefully", () => {
render(<ApiConfigManager {...defaultProps} currentApiConfigName="" />)
const addButton = screen.getByTitle('Add profile');
fireEvent.click(addButton);
const addButton = screen.getByTitle("Add profile")
fireEvent.click(addButton)
expect(mockOnUpsertConfig).toHaveBeenCalledWith(' (copy)');
});
expect(mockOnUpsertConfig).toHaveBeenCalledWith(" (copy)")
})
it('allows renaming the current config', () => {
render(<ApiConfigManager {...defaultProps} />);
// Start rename
const renameButton = screen.getByTitle('Rename profile');
fireEvent.click(renameButton);
it("allows renaming the current config", () => {
render(<ApiConfigManager {...defaultProps} />)
// Find input and enter new name
const input = screen.getByDisplayValue('Default Config');
fireEvent.input(input, { target: { value: 'New Name' } });
// Start rename
const renameButton = screen.getByTitle("Rename profile")
fireEvent.click(renameButton)
// Save
const saveButton = screen.getByTitle('Save');
fireEvent.click(saveButton);
// Find input and enter new name
const input = screen.getByDisplayValue("Default Config")
fireEvent.input(input, { target: { value: "New Name" } })
expect(mockOnRenameConfig).toHaveBeenCalledWith('Default Config', 'New Name');
});
// Save
const saveButton = screen.getByTitle("Save")
fireEvent.click(saveButton)
it('allows selecting a different config', () => {
render(<ApiConfigManager {...defaultProps} />);
const select = screen.getByRole('combobox');
fireEvent.change(select, { target: { value: 'Another Config' } });
expect(mockOnRenameConfig).toHaveBeenCalledWith("Default Config", "New Name")
})
expect(mockOnSelectConfig).toHaveBeenCalledWith('Another Config');
});
it("allows selecting a different config", () => {
render(<ApiConfigManager {...defaultProps} />)
it('allows deleting the current config when not the only one', () => {
render(<ApiConfigManager {...defaultProps} />);
const deleteButton = screen.getByTitle('Delete profile');
expect(deleteButton).not.toBeDisabled();
fireEvent.click(deleteButton);
expect(mockOnDeleteConfig).toHaveBeenCalledWith('Default Config');
});
const select = screen.getByRole("combobox")
fireEvent.change(select, { target: { value: "Another Config" } })
it('disables delete button when only one config exists', () => {
render(
<ApiConfigManager
{...defaultProps}
listApiConfigMeta={[{ name: 'Default Config' }]}
/>
);
const deleteButton = screen.getByTitle('Cannot delete the only profile');
expect(deleteButton).toHaveAttribute('disabled');
});
expect(mockOnSelectConfig).toHaveBeenCalledWith("Another Config")
})
it('cancels rename operation when clicking cancel', () => {
render(<ApiConfigManager {...defaultProps} />);
// Start rename
const renameButton = screen.getByTitle('Rename profile');
fireEvent.click(renameButton);
it("allows deleting the current config when not the only one", () => {
render(<ApiConfigManager {...defaultProps} />)
// Find input and enter new name
const input = screen.getByDisplayValue('Default Config');
fireEvent.input(input, { target: { value: 'New Name' } });
const deleteButton = screen.getByTitle("Delete profile")
expect(deleteButton).not.toBeDisabled()
// Cancel
const cancelButton = screen.getByTitle('Cancel');
fireEvent.click(cancelButton);
fireEvent.click(deleteButton)
expect(mockOnDeleteConfig).toHaveBeenCalledWith("Default Config")
})
// Verify rename was not called
expect(mockOnRenameConfig).not.toHaveBeenCalled();
// Verify we're back to normal view
expect(screen.queryByDisplayValue('New Name')).not.toBeInTheDocument();
});
});
it("disables delete button when only one config exists", () => {
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ name: "Default Config" }]} />)
const deleteButton = screen.getByTitle("Cannot delete the only profile")
expect(deleteButton).toHaveAttribute("disabled")
})
it("cancels rename operation when clicking cancel", () => {
render(<ApiConfigManager {...defaultProps} />)
// Start rename
const renameButton = screen.getByTitle("Rename profile")
fireEvent.click(renameButton)
// Find input and enter new name
const input = screen.getByDisplayValue("Default Config")
fireEvent.input(input, { target: { value: "New Name" } })
// Cancel
const cancelButton = screen.getByTitle("Cancel")
fireEvent.click(cancelButton)
// Verify rename was not called
expect(mockOnRenameConfig).not.toHaveBeenCalled()
// Verify we're back to normal view
expect(screen.queryByDisplayValue("New Name")).not.toBeInTheDocument()
})
})

View File

@@ -1,336 +1,340 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import SettingsView from '../SettingsView'
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
import { vscode } from '../../../utils/vscode'
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import SettingsView from "../SettingsView"
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
// Mock vscode API
jest.mock('../../../utils/vscode', () => ({
vscode: {
postMessage: jest.fn(),
},
jest.mock("../../../utils/vscode", () => ({
vscode: {
postMessage: jest.fn(),
},
}))
// Mock ApiConfigManager component
jest.mock('../ApiConfigManager', () => ({
__esModule: true,
default: ({ currentApiConfigName, listApiConfigMeta, onSelectConfig, onDeleteConfig, onRenameConfig, onUpsertConfig }: any) => (
<div data-testid="api-config-management">
<span>Current config: {currentApiConfigName}</span>
</div>
)
jest.mock("../ApiConfigManager", () => ({
__esModule: true,
default: ({
currentApiConfigName,
listApiConfigMeta,
onSelectConfig,
onDeleteConfig,
onRenameConfig,
onUpsertConfig,
}: any) => (
<div data-testid="api-config-management">
<span>Current config: {currentApiConfigName}</span>
</div>
),
}))
// Mock VSCode components
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
VSCodeButton: ({ children, onClick, appearance }: any) => (
appearance === 'icon' ?
<button onClick={onClick} className="codicon codicon-close" aria-label="Remove command">
<span className="codicon codicon-close" />
</button> :
<button onClick={onClick} data-appearance={appearance}>{children}</button>
),
VSCodeCheckbox: ({ children, onChange, checked }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
aria-label={typeof children === 'string' ? children : undefined}
/>
{children}
</label>
),
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
<input
type="text"
value={value}
onChange={(e) => onInput({ target: { value: e.target.value } })}
placeholder={placeholder}
/>
),
VSCodeTextArea: () => <textarea />,
VSCodeLink: ({ children, href }: any) => <a href={href || '#'}>{children}</a>,
VSCodeDropdown: ({ children, value, onChange }: any) => (
<select value={value} onChange={onChange}>
{children}
</select>
),
VSCodeOption: ({ children, value }: any) => (
<option value={value}>{children}</option>
),
VSCodeRadio: ({ children, value, checked, onChange }: any) => (
<input
type="radio"
value={value}
checked={checked}
onChange={onChange}
/>
),
VSCodeRadioGroup: ({ children, value, onChange }: any) => (
<div onChange={onChange}>
{children}
</div>
),
VSCodeSlider: ({ value, onChange }: any) => (
<input
type="range"
value={value}
onChange={(e) => onChange({ target: { value: Number(e.target.value) } })}
min={0}
max={1}
step={0.01}
style={{ flexGrow: 1, height: '2px' }}
/>
)
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeButton: ({ children, onClick, appearance }: any) =>
appearance === "icon" ? (
<button onClick={onClick} className="codicon codicon-close" aria-label="Remove command">
<span className="codicon codicon-close" />
</button>
) : (
<button onClick={onClick} data-appearance={appearance}>
{children}
</button>
),
VSCodeCheckbox: ({ children, onChange, checked }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
aria-label={typeof children === "string" ? children : undefined}
/>
{children}
</label>
),
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
<input
type="text"
value={value}
onChange={(e) => onInput({ target: { value: e.target.value } })}
placeholder={placeholder}
/>
),
VSCodeTextArea: () => <textarea />,
VSCodeLink: ({ children, href }: any) => <a href={href || "#"}>{children}</a>,
VSCodeDropdown: ({ children, value, onChange }: any) => (
<select value={value} onChange={onChange}>
{children}
</select>
),
VSCodeOption: ({ children, value }: any) => <option value={value}>{children}</option>,
VSCodeRadio: ({ children, value, checked, onChange }: any) => (
<input type="radio" value={value} checked={checked} onChange={onChange} />
),
VSCodeRadioGroup: ({ children, value, onChange }: any) => <div onChange={onChange}>{children}</div>,
VSCodeSlider: ({ value, onChange }: any) => (
<input
type="range"
value={value}
onChange={(e) => onChange({ target: { value: Number(e.target.value) } })}
min={0}
max={1}
step={0.01}
style={{ flexGrow: 1, height: "2px" }}
/>
),
}))
// Mock window.postMessage to trigger state hydration
const mockPostMessage = (state: any) => {
window.postMessage({
type: 'state',
state: {
version: '1.0.0',
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
allowedCommands: [],
alwaysAllowExecute: false,
soundEnabled: false,
soundVolume: 0.5,
...state
}
}, '*')
window.postMessage(
{
type: "state",
state: {
version: "1.0.0",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
allowedCommands: [],
alwaysAllowExecute: false,
soundEnabled: false,
soundVolume: 0.5,
...state,
},
},
"*",
)
}
const renderSettingsView = () => {
const onDone = jest.fn()
render(
<ExtensionStateContextProvider>
<SettingsView onDone={onDone} />
</ExtensionStateContextProvider>
)
// Hydrate initial state
mockPostMessage({})
return { onDone }
const onDone = jest.fn()
render(
<ExtensionStateContextProvider>
<SettingsView onDone={onDone} />
</ExtensionStateContextProvider>,
)
// Hydrate initial state
mockPostMessage({})
return { onDone }
}
describe('SettingsView - Sound Settings', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe("SettingsView - Sound Settings", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('initializes with sound disabled by default', () => {
renderSettingsView()
const soundCheckbox = screen.getByRole('checkbox', {
name: /Enable sound effects/i
})
expect(soundCheckbox).not.toBeChecked()
// Volume slider should not be visible when sound is disabled
expect(screen.queryByRole('slider', { name: /volume/i })).not.toBeInTheDocument()
})
it("initializes with sound disabled by default", () => {
renderSettingsView()
it('toggles sound setting and sends message to VSCode', () => {
renderSettingsView()
const soundCheckbox = screen.getByRole('checkbox', {
name: /Enable sound effects/i
})
// Enable sound
fireEvent.click(soundCheckbox)
expect(soundCheckbox).toBeChecked()
// Click Done to save settings
const doneButton = screen.getByText('Done')
fireEvent.click(doneButton)
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: 'soundEnabled',
bool: true
})
)
})
const soundCheckbox = screen.getByRole("checkbox", {
name: /Enable sound effects/i,
})
expect(soundCheckbox).not.toBeChecked()
it('shows volume slider when sound is enabled', () => {
renderSettingsView()
// Enable sound
const soundCheckbox = screen.getByRole('checkbox', {
name: /Enable sound effects/i
})
fireEvent.click(soundCheckbox)
// Volume slider should not be visible when sound is disabled
expect(screen.queryByRole("slider", { name: /volume/i })).not.toBeInTheDocument()
})
// Volume slider should be visible
const volumeSlider = screen.getByRole('slider', { name: /volume/i })
expect(volumeSlider).toBeInTheDocument()
expect(volumeSlider).toHaveValue('0.5')
})
it("toggles sound setting and sends message to VSCode", () => {
renderSettingsView()
it('updates volume and sends message to VSCode when slider changes', () => {
renderSettingsView()
// Enable sound
const soundCheckbox = screen.getByRole('checkbox', {
name: /Enable sound effects/i
})
fireEvent.click(soundCheckbox)
const soundCheckbox = screen.getByRole("checkbox", {
name: /Enable sound effects/i,
})
// Change volume
const volumeSlider = screen.getByRole('slider', { name: /volume/i })
fireEvent.change(volumeSlider, { target: { value: '0.75' } })
// Enable sound
fireEvent.click(soundCheckbox)
expect(soundCheckbox).toBeChecked()
// Click Done to save settings
const doneButton = screen.getByText('Done')
fireEvent.click(doneButton)
// Click Done to save settings
const doneButton = screen.getByText("Done")
fireEvent.click(doneButton)
// Verify message sent to VSCode
expect(vscode.postMessage).toHaveBeenCalledWith({
type: 'soundVolume',
value: 0.75
})
})
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "soundEnabled",
bool: true,
}),
)
})
it("shows volume slider when sound is enabled", () => {
renderSettingsView()
// Enable sound
const soundCheckbox = screen.getByRole("checkbox", {
name: /Enable sound effects/i,
})
fireEvent.click(soundCheckbox)
// Volume slider should be visible
const volumeSlider = screen.getByRole("slider", { name: /volume/i })
expect(volumeSlider).toBeInTheDocument()
expect(volumeSlider).toHaveValue("0.5")
})
it("updates volume and sends message to VSCode when slider changes", () => {
renderSettingsView()
// Enable sound
const soundCheckbox = screen.getByRole("checkbox", {
name: /Enable sound effects/i,
})
fireEvent.click(soundCheckbox)
// Change volume
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)
// Verify message sent to VSCode
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "soundVolume",
value: 0.75,
})
})
})
describe('SettingsView - API Configuration', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe("SettingsView - API Configuration", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('renders ApiConfigManagement with correct props', () => {
renderSettingsView()
expect(screen.getByTestId('api-config-management')).toBeInTheDocument()
})
it("renders ApiConfigManagement with correct props", () => {
renderSettingsView()
expect(screen.getByTestId("api-config-management")).toBeInTheDocument()
})
})
describe('SettingsView - Allowed Commands', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe("SettingsView - Allowed Commands", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('shows allowed commands section when alwaysAllowExecute is enabled', () => {
renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve allowed execute operations/i
})
fireEvent.click(executeCheckbox)
it("shows allowed commands section when alwaysAllowExecute is enabled", () => {
renderSettingsView()
// Verify allowed commands section appears
expect(screen.getByText(/Allowed Auto-Execute Commands/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/Enter command prefix/i)).toBeInTheDocument()
})
// Enable always allow execute
const executeCheckbox = screen.getByRole("checkbox", {
name: /Always approve allowed execute operations/i,
})
fireEvent.click(executeCheckbox)
it('adds new command to the list', () => {
renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve allowed execute operations/i
})
fireEvent.click(executeCheckbox)
// Verify allowed commands section appears
expect(screen.getByText(/Allowed Auto-Execute Commands/i)).toBeInTheDocument()
expect(screen.getByPlaceholderText(/Enter command prefix/i)).toBeInTheDocument()
})
// Add a new command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: 'npm test' } })
const addButton = screen.getByText('Add')
fireEvent.click(addButton)
it("adds new command to the list", () => {
renderSettingsView()
// Verify command was added
expect(screen.getByText('npm test')).toBeInTheDocument()
// Verify VSCode message was sent
expect(vscode.postMessage).toHaveBeenCalledWith({
type: 'allowedCommands',
commands: ['npm test']
})
})
// Enable always allow execute
const executeCheckbox = screen.getByRole("checkbox", {
name: /Always approve allowed execute operations/i,
})
fireEvent.click(executeCheckbox)
it('removes command from the list', () => {
renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve allowed execute operations/i
})
fireEvent.click(executeCheckbox)
// Add a new command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: "npm test" } })
// Add a command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: 'npm test' } })
const addButton = screen.getByText('Add')
fireEvent.click(addButton)
const addButton = screen.getByText("Add")
fireEvent.click(addButton)
// Remove the command
const removeButton = screen.getByRole('button', { name: 'Remove command' })
fireEvent.click(removeButton)
// Verify command was added
expect(screen.getByText("npm test")).toBeInTheDocument()
// Verify command was removed
expect(screen.queryByText('npm test')).not.toBeInTheDocument()
// Verify VSCode message was sent
expect(vscode.postMessage).toHaveBeenLastCalledWith({
type: 'allowedCommands',
commands: []
})
})
// Verify VSCode message was sent
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "allowedCommands",
commands: ["npm test"],
})
})
it('prevents duplicate commands', () => {
renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve allowed execute operations/i
})
fireEvent.click(executeCheckbox)
it("removes command from the list", () => {
renderSettingsView()
// Add a command twice
const input = screen.getByPlaceholderText(/Enter command prefix/i)
const addButton = screen.getByText('Add')
// Enable always allow execute
const executeCheckbox = screen.getByRole("checkbox", {
name: /Always approve allowed execute operations/i,
})
fireEvent.click(executeCheckbox)
// First addition
fireEvent.change(input, { target: { value: 'npm test' } })
fireEvent.click(addButton)
// Add a command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: "npm test" } })
const addButton = screen.getByText("Add")
fireEvent.click(addButton)
// Second addition attempt
fireEvent.change(input, { target: { value: 'npm test' } })
fireEvent.click(addButton)
// Remove the command
const removeButton = screen.getByRole("button", { name: "Remove command" })
fireEvent.click(removeButton)
// Verify command appears only once
const commands = screen.getAllByText('npm test')
expect(commands).toHaveLength(1)
})
// Verify command was removed
expect(screen.queryByText("npm test")).not.toBeInTheDocument()
it('saves allowed commands when clicking Done', () => {
const { onDone } = renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve allowed execute operations/i
})
fireEvent.click(executeCheckbox)
// Verify VSCode message was sent
expect(vscode.postMessage).toHaveBeenLastCalledWith({
type: "allowedCommands",
commands: [],
})
})
// Add a command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: 'npm test' } })
const addButton = screen.getByText('Add')
fireEvent.click(addButton)
it("prevents duplicate commands", () => {
renderSettingsView()
// Click Done
const doneButton = screen.getByText('Done')
fireEvent.click(doneButton)
// Enable always allow execute
const executeCheckbox = screen.getByRole("checkbox", {
name: /Always approve allowed execute operations/i,
})
fireEvent.click(executeCheckbox)
// Verify VSCode messages were sent
expect(vscode.postMessage).toHaveBeenCalledWith(expect.objectContaining({
type: 'allowedCommands',
commands: ['npm test']
}))
expect(onDone).toHaveBeenCalled()
})
// Add a command twice
const input = screen.getByPlaceholderText(/Enter command prefix/i)
const addButton = screen.getByText("Add")
// First addition
fireEvent.change(input, { target: { value: "npm test" } })
fireEvent.click(addButton)
// Second addition attempt
fireEvent.change(input, { target: { value: "npm test" } })
fireEvent.click(addButton)
// Verify command appears only once
const commands = screen.getAllByText("npm test")
expect(commands).toHaveLength(1)
})
it("saves allowed commands when clicking Done", () => {
const { onDone } = renderSettingsView()
// Enable always allow execute
const executeCheckbox = screen.getByRole("checkbox", {
name: /Always approve allowed execute operations/i,
})
fireEvent.click(executeCheckbox)
// Add a command
const input = screen.getByPlaceholderText(/Enter command prefix/i)
fireEvent.change(input, { target: { value: "npm test" } })
const addButton = screen.getByText("Add")
fireEvent.click(addButton)
// Click Done
const doneButton = screen.getByText("Done")
fireEvent.click(doneButton)
// Verify VSCode messages were sent
expect(vscode.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "allowedCommands",
commands: ["npm test"],
}),
)
expect(onDone).toHaveBeenCalled()
})
})