Skip to content

Commit 1a3b870

Browse files
committed
Improved error message feedback in settings panel
1 parent f065f03 commit 1a3b870

File tree

7 files changed

+98
-90
lines changed

7 files changed

+98
-90
lines changed

webview-ui/src/__mocks__/vscrui.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const Dropdown = ({ children, value, onChange }: any) =>
88

99
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
1010

11+
export const Button = ({ children, ...props }: any) =>
12+
React.createElement("div", { "data-testid": "mock-button", ...props }, children)
13+
1114
export type DropdownOption = {
1215
label: string
1316
value: string
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from "react"
2+
3+
interface ApiErrorMessageProps {
4+
errorMessage: string | undefined
5+
children?: React.ReactNode
6+
}
7+
const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
8+
return (
9+
<div className="text-vscode-errorForeground text-sm">
10+
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
11+
{errorMessage}
12+
{children}
13+
</div>
14+
)
15+
}
16+
export default ApiErrorMessage

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo, useCallback, useMemo, useState } from "react"
1+
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
22
import { useDebounce, useEvent } from "react-use"
33
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
44
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
@@ -42,23 +42,25 @@ import { ModelInfoView } from "./ModelInfoView"
4242
import { DROPDOWN_Z_INDEX } from "./styles"
4343
import { ModelPicker } from "./ModelPicker"
4444
import { TemperatureControl } from "./TemperatureControl"
45+
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
46+
import ApiErrorMessage from "./ApiErrorMessage"
4547

4648
interface ApiOptionsProps {
4749
uriScheme: string | undefined
4850
apiConfiguration: ApiConfiguration
4951
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
50-
apiErrorMessage?: string
51-
modelIdErrorMessage?: string
5252
fromWelcomeView?: boolean
53+
errorMessage: string | undefined
54+
setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
5355
}
5456

5557
const ApiOptions = ({
5658
uriScheme,
5759
apiConfiguration,
5860
setApiConfigurationField,
59-
apiErrorMessage,
60-
modelIdErrorMessage,
6161
fromWelcomeView,
62+
errorMessage,
63+
setErrorMessage,
6264
}: ApiOptionsProps) => {
6365
const [ollamaModels, setOllamaModels] = useState<string[]>([])
6466
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
@@ -146,6 +148,13 @@ const ApiOptions = ({
146148
],
147149
)
148150

151+
useEffect(() => {
152+
const apiValidationResult =
153+
validateApiConfiguration(apiConfiguration) ||
154+
validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
155+
setErrorMessage(apiValidationResult)
156+
}, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
157+
149158
const handleMessage = useCallback((event: MessageEvent) => {
150159
const message: ExtensionMessage = event.data
151160
switch (message.type) {
@@ -626,6 +635,7 @@ const ApiOptions = ({
626635
]}
627636
/>
628637
</div>
638+
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
629639
<p
630640
style={{
631641
fontSize: "12px",
@@ -705,6 +715,7 @@ const ApiOptions = ({
705715
models={openAiModels}
706716
setApiConfigurationField={setApiConfigurationField}
707717
defaultModelInfo={openAiModelInfoSaneDefaults}
718+
errorMessage={errorMessage}
708719
/>
709720
<div style={{ display: "flex", alignItems: "center" }}>
710721
<Checkbox
@@ -1068,18 +1079,6 @@ const ApiOptions = ({
10681079
/>
10691080

10701081
{/* end Model Info Configuration */}
1071-
1072-
<p
1073-
style={{
1074-
fontSize: "12px",
1075-
marginTop: 3,
1076-
color: "var(--vscode-descriptionForeground)",
1077-
}}>
1078-
<span style={{ color: "var(--vscode-errorForeground)" }}>
1079-
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
1080-
with Claude models. Less capable models may not work as expected.)
1081-
</span>
1082-
</p>
10831082
</div>
10841083
)}
10851084

@@ -1100,6 +1099,7 @@ const ApiOptions = ({
11001099
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
11011100
<span style={{ fontWeight: 500 }}>Model ID</span>
11021101
</VSCodeTextField>
1102+
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
11031103

11041104
{lmStudioModels.length > 0 && (
11051105
<VSCodeRadioGroup
@@ -1245,6 +1245,12 @@ const ApiOptions = ({
12451245
placeholder={"e.g. llama3.1"}>
12461246
<span style={{ fontWeight: 500 }}>Model ID</span>
12471247
</VSCodeTextField>
1248+
{errorMessage && (
1249+
<div className="text-vscode-errorForeground text-sm">
1250+
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
1251+
{errorMessage}
1252+
</div>
1253+
)}
12481254
{ollamaModels.length > 0 && (
12491255
<VSCodeRadioGroup
12501256
value={
@@ -1321,22 +1327,11 @@ const ApiOptions = ({
13211327
serviceUrl="https://api.getunbound.ai/models"
13221328
recommendedModel={unboundDefaultModelId}
13231329
setApiConfigurationField={setApiConfigurationField}
1330+
errorMessage={errorMessage}
13241331
/>
13251332
</div>
13261333
)}
13271334

1328-
{apiErrorMessage && (
1329-
<p
1330-
style={{
1331-
margin: "-10px 0 4px 0",
1332-
fontSize: 12,
1333-
color: "var(--vscode-errorForeground)",
1334-
}}>
1335-
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
1336-
{apiErrorMessage}
1337-
</p>
1338-
)}
1339-
13401335
{selectedProvider === "glama" && (
13411336
<ModelPicker
13421337
apiConfiguration={apiConfiguration ?? {}}
@@ -1349,6 +1344,7 @@ const ApiOptions = ({
13491344
serviceUrl="https://glama.ai/models"
13501345
recommendedModel="anthropic/claude-3-7-sonnet"
13511346
setApiConfigurationField={setApiConfigurationField}
1347+
errorMessage={errorMessage}
13521348
/>
13531349
)}
13541350

@@ -1364,6 +1360,7 @@ const ApiOptions = ({
13641360
serviceName="OpenRouter"
13651361
serviceUrl="https://openrouter.ai/models"
13661362
recommendedModel="anthropic/claude-3.7-sonnet"
1363+
errorMessage={errorMessage}
13671364
/>
13681365
)}
13691366
{selectedProvider === "requesty" && (
@@ -1378,6 +1375,7 @@ const ApiOptions = ({
13781375
serviceName="Requesty"
13791376
serviceUrl="https://requesty.ai"
13801377
recommendedModel="anthropic/claude-3-7-sonnet-latest"
1378+
errorMessage={errorMessage}
13811379
/>
13821380
)}
13831381

@@ -1401,6 +1399,7 @@ const ApiOptions = ({
14011399
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
14021400
{selectedProvider === "mistral" && createDropdown(mistralModels)}
14031401
</div>
1402+
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
14041403
<ModelInfoView
14051404
selectedModelId={selectedModelId}
14061405
modelInfo={selectedModelInfo}
@@ -1448,18 +1447,6 @@ const ApiOptions = ({
14481447
/>
14491448
</div>
14501449
)}
1451-
1452-
{modelIdErrorMessage && (
1453-
<p
1454-
style={{
1455-
margin: "-10px 0 4px 0",
1456-
fontSize: 12,
1457-
color: "var(--vscode-errorForeground)",
1458-
}}>
1459-
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
1460-
{modelIdErrorMessage}
1461-
</p>
1462-
)}
14631450
</div>
14641451
)
14651452
}

webview-ui/src/components/settings/ModelPicker.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { normalizeApiConfiguration } from "./ApiOptions"
55
import { ModelInfoView } from "./ModelInfoView"
66
import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
77
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
8+
import ApiErrorMessage from "./ApiErrorMessage"
89

910
type ExtractType<T> = NonNullable<
1011
{ [K in keyof ApiConfiguration]: Required<ApiConfiguration>[K] extends T ? K : never }[keyof ApiConfiguration]
@@ -30,6 +31,7 @@ interface ModelPickerProps {
3031
apiConfiguration: ApiConfiguration
3132
setApiConfigurationField: <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => void
3233
defaultModelInfo?: ModelInfo
34+
errorMessage?: string
3335
}
3436

3537
export const ModelPicker = ({
@@ -43,6 +45,7 @@ export const ModelPicker = ({
4345
apiConfiguration,
4446
setApiConfigurationField,
4547
defaultModelInfo,
48+
errorMessage,
4649
}: ModelPickerProps) => {
4750
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
4851

@@ -69,11 +72,16 @@ export const ModelPicker = ({
6972
return (
7073
<>
7174
<div className="font-semibold">Model</div>
72-
<Combobox type="single" inputValue={apiConfiguration[modelIdKey]} onInputValueChange={onSelect}>
75+
<Combobox
76+
style={errorMessage ? { "--color-vscode-dropdown-border": "var(--color-vscode-errorForeground)" } : {}}
77+
type="single"
78+
inputValue={apiConfiguration[modelIdKey]}
79+
onInputValueChange={onSelect}>
7380
<ComboboxInput
7481
className="border-vscode-errorForeground tefat"
7582
placeholder="Search model..."
7683
data-testid="model-input"
84+
aria-errormessage={errorMessage}
7785
/>
7886
<ComboboxContent>
7987
<ComboboxEmpty>No model found.</ComboboxEmpty>
@@ -85,13 +93,30 @@ export const ModelPicker = ({
8593
</ComboboxContent>
8694
</Combobox>
8795

88-
{selectedModelId && selectedModelInfo && (
89-
<ModelInfoView
90-
selectedModelId={selectedModelId}
91-
modelInfo={selectedModelInfo}
92-
isDescriptionExpanded={isDescriptionExpanded}
93-
setIsDescriptionExpanded={setIsDescriptionExpanded}
94-
/>
96+
{errorMessage ? (
97+
<ApiErrorMessage errorMessage={errorMessage}>
98+
<p
99+
style={{
100+
fontSize: "12px",
101+
marginTop: 3,
102+
color: "var(--vscode-descriptionForeground)",
103+
}}>
104+
<span style={{ color: "var(--vscode-errorForeground)" }}>
105+
<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
106+
with Claude models. Less capable models may not work as expected.
107+
</span>
108+
</p>
109+
</ApiErrorMessage>
110+
) : (
111+
selectedModelId &&
112+
selectedModelInfo && (
113+
<ModelInfoView
114+
selectedModelId={selectedModelId}
115+
modelInfo={selectedModelInfo}
116+
isDescriptionExpanded={isDescriptionExpanded}
117+
setIsDescriptionExpanded={setIsDescriptionExpanded}
118+
/>
119+
)
95120
)}
96121
<p>
97122
The extension automatically fetches the latest list of models available on{" "}

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
22
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3-
import { Dropdown, type DropdownOption } from "vscrui"
3+
import { Button, Dropdown, type DropdownOption } from "vscrui"
44

55
import {
66
AlertDialog,
@@ -14,7 +14,6 @@ import {
1414
} from "@/components/ui"
1515

1616
import { vscode } from "../../utils/vscode"
17-
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
1817
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
1918
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
2019
import { ApiConfiguration } from "../../../../src/shared/api"
@@ -33,14 +32,13 @@ export interface SettingsViewRef {
3332

3433
const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone }, ref) => {
3534
const extensionState = useExtensionState()
36-
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
37-
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
3835
const [commandInput, setCommandInput] = useState("")
3936
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
4037
const [cachedState, setCachedState] = useState(extensionState)
4138
const [isChangeDetected, setChangeDetected] = useState(false)
4239
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
4340
const confirmDialogHandler = useRef<() => void>()
41+
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
4442

4543
// TODO: Reduce WebviewMessage/ExtensionState complexity
4644
const { currentApiConfigName } = extensionState
@@ -135,20 +133,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
135133
}
136134
})
137135
}, [])
138-
136+
const isSettingValid = !errorMessage
139137
const handleSubmit = () => {
140-
const apiValidationResult = validateApiConfiguration(apiConfiguration)
141-
142-
const modelIdValidationResult = validateModelId(
143-
apiConfiguration,
144-
extensionState.glamaModels,
145-
extensionState.openRouterModels,
146-
)
147-
148-
setApiErrorMessage(apiValidationResult)
149-
setModelIdErrorMessage(modelIdValidationResult)
150-
151-
if (!apiValidationResult && !modelIdValidationResult) {
138+
if (isSettingValid) {
152139
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
153140
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
154141
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -177,23 +164,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
177164
}
178165
}
179166

180-
useEffect(() => {
181-
setApiErrorMessage(undefined)
182-
setModelIdErrorMessage(undefined)
183-
}, [apiConfiguration])
184-
185-
// Initial validation on mount
186-
useEffect(() => {
187-
const apiValidationResult = validateApiConfiguration(apiConfiguration)
188-
const modelIdValidationResult = validateModelId(
189-
apiConfiguration,
190-
extensionState.glamaModels,
191-
extensionState.openRouterModels,
192-
)
193-
setApiErrorMessage(apiValidationResult)
194-
setModelIdErrorMessage(modelIdValidationResult)
195-
}, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
196-
197167
const checkUnsaveChanges = useCallback(
198168
(then: () => void) => {
199169
if (isChangeDetected) {
@@ -287,13 +257,14 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
287257
justifyContent: "space-between",
288258
gap: "6px",
289259
}}>
290-
<VSCodeButton
291-
appearance="primary"
292-
title={isChangeDetected ? "Save changes" : "Nothing changed"}
260+
<Button
261+
appearance={isSettingValid ? "primary" : "secondary"}
262+
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
263+
title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
293264
onClick={handleSubmit}
294-
disabled={!isChangeDetected}>
265+
disabled={!isChangeDetected || !isSettingValid}>
295266
Save
296-
</VSCodeButton>
267+
</Button>
297268
<VSCodeButton
298269
appearance="secondary"
299270
title="Discard unsaved changes and close settings panel"
@@ -344,8 +315,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
344315
uriScheme={extensionState.uriScheme}
345316
apiConfiguration={apiConfiguration}
346317
setApiConfigurationField={setApiConfigurationField}
347-
apiErrorMessage={apiErrorMessage}
348-
modelIdErrorMessage={modelIdErrorMessage}
318+
errorMessage={errorMessage}
319+
setErrorMessage={setErrorMessage}
349320
/>
350321
</div>
351322
</div>

0 commit comments

Comments
 (0)