mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-22 05:11:06 -05:00
Prettier backfill
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user