From 6c6087e9a2d7c200cdbd19599b328b88ba9af45e Mon Sep 17 00:00:00 2001 From: cte Date: Mon, 24 Feb 2025 23:53:01 -0800 Subject: [PATCH 01/32] v3.7.3 --- .changeset/real-zebras-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/real-zebras-remain.md diff --git a/.changeset/real-zebras-remain.md b/.changeset/real-zebras-remain.md new file mode 100644 index 0000000000..94a1dc5d36 --- /dev/null +++ b/.changeset/real-zebras-remain.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +v3.7.3 From 2655bebc8fb9abc1ebd923249286bf92b40e72ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Feb 2025 07:57:37 +0000 Subject: [PATCH 02/32] changeset version bump --- .changeset/real-zebras-remain.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/real-zebras-remain.md diff --git a/.changeset/real-zebras-remain.md b/.changeset/real-zebras-remain.md deleted file mode 100644 index 94a1dc5d36..0000000000 --- a/.changeset/real-zebras-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -v3.7.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 381e0907eb..df70430226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.7.3 + +### Patch Changes + +- v3.7.3 + ## [3.7.2] - Fix computer use and prompt caching for OpenRouter's `anthropic/claude-3.7-sonnet:beta` (thanks @cte!) diff --git a/package-lock.json b/package-lock.json index ba152e81f3..71e3aa8648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.7.2", + "version": "3.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", diff --git a/package.json b/package.json index 651de2b764..5166affcaa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "An AI-powered autonomous coding agent that lives in your editor.", "publisher": "RooVeterinaryInc", - "version": "3.7.2", + "version": "3.7.3", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 8ba179d3b00fbdb528738b996fad6e88eab9fd43 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 25 Feb 2025 07:58:01 +0000 Subject: [PATCH 03/32] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df70430226..f3a0db08a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.7.3 - -### Patch Changes +## [3.7.3] - v3.7.3 From be7f71c49890d7176c19e7b14da16493557437e5 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Mon, 24 Feb 2025 23:58:45 -0800 Subject: [PATCH 04/32] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a0db08a2..fc9b387f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [3.7.3] -- v3.7.3 +- Support for Sonnet 3.7 "Thinking". ## [3.7.2] From 613f74b717247ff4112452ff9152207418c4e71a Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Tue, 25 Feb 2025 00:05:04 -0800 Subject: [PATCH 05/32] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9b387f7f..f72dd8a0ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [3.7.3] -- Support for Sonnet 3.7 "Thinking". +- Support for ["Thinking"](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) Sonnet 3.7 when using the Anthropic provider. ## [3.7.2] From c6092ce170492ae99bbb3fffe39706212474f17f Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 01:03:12 -0800 Subject: [PATCH 06/32] Properly reset thinking setting when changing profiles --- .../src/components/settings/ApiOptions.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 0b6a118656..8c2f382db6 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -68,15 +68,17 @@ const ApiOptions = ({ const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) - const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any + const anthropicThinkingBudget = apiConfiguration?.anthropicThinking + const noTransform = (value: T) => value + const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any const dropdownEventTransform = (event: DropdownOption | string | undefined) => (typeof event == "string" ? event : event?.value) as T + const handleInputChange = useCallback( ( field: K, @@ -107,8 +109,10 @@ const ApiOptions = ({ 250, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl], ) + const handleMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data + if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) { const newModels = message.ollamaModels setOllamaModels(newModels) @@ -120,6 +124,7 @@ const ApiOptions = ({ setVsCodeLmModels(newModels) } }, []) + useEvent("message", handleMessage) const createDropdown = (models: Record) => { @@ -130,6 +135,7 @@ const ApiOptions = ({ label: modelId, })), ] + return ( { - const budget = checked ? 16_384 : undefined - setAnthropicThinkingBudget(budget) - setApiConfigurationField("anthropicThinking", budget) - }}> + onChange={(checked) => + setApiConfigurationField("anthropicThinking", checked ? 16_384 : undefined) + }> Thinking? {anthropicThinkingBudget && ( @@ -1286,11 +1290,7 @@ const ApiOptions = ({ max={anthropicModels["claude-3-7-sonnet-20250219"].maxTokens - 1} step={1024} value={[anthropicThinkingBudget]} - onValueChange={(value) => { - const budget = value[0] - setAnthropicThinkingBudget(budget) - setApiConfigurationField("anthropicThinking", budget) - }} + onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])} />
{anthropicThinkingBudget}
From 3621deb725937a5b97fcc62a49c7e464b67501f1 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 01:04:31 -0800 Subject: [PATCH 07/32] Add changeset --- .changeset/little-deers-occur.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-deers-occur.md diff --git a/.changeset/little-deers-occur.md b/.changeset/little-deers-occur.md new file mode 100644 index 0000000000..e699d277c1 --- /dev/null +++ b/.changeset/little-deers-occur.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. From 53ead4c886b236db2d744e1c5148112a246aff18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Feb 2025 14:19:17 +0000 Subject: [PATCH 08/32] changeset version bump --- .changeset/little-deers-occur.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/little-deers-occur.md diff --git a/.changeset/little-deers-occur.md b/.changeset/little-deers-occur.md deleted file mode 100644 index e699d277c1..0000000000 --- a/.changeset/little-deers-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. diff --git a/CHANGELOG.md b/CHANGELOG.md index f72dd8a0ca..2586e8540f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.7.4 + +### Patch Changes + +- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. + ## [3.7.3] - Support for ["Thinking"](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) Sonnet 3.7 when using the Anthropic provider. diff --git a/package-lock.json b/package-lock.json index 71e3aa8648..4bcdf8136d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.7.3", + "version": "3.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.7.3", + "version": "3.7.4", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", diff --git a/package.json b/package.json index 5166affcaa..84bec2645a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "An AI-powered autonomous coding agent that lives in your editor.", "publisher": "RooVeterinaryInc", - "version": "3.7.3", + "version": "3.7.4", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From df4d2a549c6ac1464de40cfc8bd8340da5ead9d1 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 25 Feb 2025 14:19:45 +0000 Subject: [PATCH 09/32] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2586e8540f..52fb754097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.7.4 - -### Patch Changes +## [3.7.4] - Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. From d5b796263dd160d4f6fe0ccc0ad8c464c97ec01f Mon Sep 17 00:00:00 2001 From: System233 Date: Wed, 26 Feb 2025 02:44:57 +0800 Subject: [PATCH 10/32] Add a combobox component with auto-complete functionality --- webview-ui/src/__mocks__/lucide-react.ts | 6 + .../src/components/ui/combobox-primitive.tsx | 522 ++++++++++++++++++ webview-ui/src/components/ui/combobox.tsx | 177 ++++++ webview-ui/src/components/ui/input-base.tsx | 157 ++++++ 4 files changed, 862 insertions(+) create mode 100644 webview-ui/src/__mocks__/lucide-react.ts create mode 100644 webview-ui/src/components/ui/combobox-primitive.tsx create mode 100644 webview-ui/src/components/ui/combobox.tsx create mode 100644 webview-ui/src/components/ui/input-base.tsx diff --git a/webview-ui/src/__mocks__/lucide-react.ts b/webview-ui/src/__mocks__/lucide-react.ts new file mode 100644 index 0000000000..d85cd25d6a --- /dev/null +++ b/webview-ui/src/__mocks__/lucide-react.ts @@ -0,0 +1,6 @@ +import React from "react" + +export const Check = () => React.createElement("div") +export const ChevronsUpDown = () => React.createElement("div") +export const Loader = () => React.createElement("div") +export const X = () => React.createElement("div") diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx new file mode 100644 index 0000000000..13bad87aba --- /dev/null +++ b/webview-ui/src/components/ui/combobox-primitive.tsx @@ -0,0 +1,522 @@ +/* eslint-disable react/jsx-pascal-case */ +"use client" + +import * as React from "react" +import { composeEventHandlers } from "@radix-ui/primitive" +import { useComposedRefs } from "@radix-ui/react-compose-refs" +import * as PopoverPrimitive from "@radix-ui/react-popover" +import { Primitive } from "@radix-ui/react-primitive" +import * as RovingFocusGroupPrimitive from "@radix-ui/react-roving-focus" +import { useControllableState } from "@radix-ui/react-use-controllable-state" +import { Command as CommandPrimitive } from "cmdk" + +export type ComboboxContextProps = { + inputValue: string + onInputValueChange: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void + onInputBlur?: (e: React.FocusEvent) => void + open: boolean + onOpenChange: (open: boolean) => void + currentTabStopId: string | null + onCurrentTabStopIdChange: (currentTabStopId: string | null) => void + inputRef: React.RefObject + tagGroupRef: React.RefObject> + disabled?: boolean + required?: boolean +} & ( + | Required> + | Required> +) + +const ComboboxContext = React.createContext({ + type: "single", + value: "", + onValueChange: () => {}, + inputValue: "", + onInputValueChange: () => {}, + onInputBlur: () => {}, + open: false, + onOpenChange: () => {}, + currentTabStopId: null, + onCurrentTabStopIdChange: () => {}, + inputRef: { current: null }, + tagGroupRef: { current: null }, + disabled: false, + required: false, +}) + +export const useComboboxContext = () => React.useContext(ComboboxContext) + +export type ComboboxType = "single" | "multiple" + +export interface ComboboxBaseProps + extends React.ComponentProps, + Omit, "value" | "defaultValue" | "onValueChange"> { + type?: ComboboxType | undefined + inputValue?: string + defaultInputValue?: string + onInputValueChange?: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void + onInputBlur?: (e: React.FocusEvent) => void + disabled?: boolean + required?: boolean +} + +export type ComboboxValue = T extends "single" + ? string + : T extends "multiple" + ? string[] + : never + +export interface ComboboxSingleProps { + type: "single" + value?: string + defaultValue?: string + onValueChange?: (value: string) => void +} + +export interface ComboboxMultipleProps { + type: "multiple" + value?: string[] + defaultValue?: string[] + onValueChange?: (value: string[]) => void +} + +export type ComboboxProps = ComboboxBaseProps & (ComboboxSingleProps | ComboboxMultipleProps) + +export const Combobox = React.forwardRef( + ( + { + type = "single" as T, + open: openProp, + onOpenChange, + defaultOpen, + modal, + children, + value: valueProp, + defaultValue, + onValueChange, + inputValue: inputValueProp, + defaultInputValue, + onInputValueChange, + onInputBlur, + disabled, + required, + ...props + }: ComboboxProps, + ref: React.ForwardedRef>, + ) => { + const [value = type === "multiple" ? [] : "", setValue] = useControllableState>({ + prop: valueProp as ComboboxValue, + defaultProp: defaultValue as ComboboxValue, + onChange: onValueChange as (value: ComboboxValue) => void, + }) + const [inputValue = "", setInputValue] = useControllableState({ + prop: inputValueProp, + defaultProp: defaultInputValue, + }) + const [open = false, setOpen] = useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChange, + }) + const [currentTabStopId, setCurrentTabStopId] = React.useState(null) + const inputRef = React.useRef(null) + const tagGroupRef = React.useRef>(null) + + const handleInputValueChange: ComboboxContextProps["onInputValueChange"] = React.useCallback( + (inputValue, reason) => { + setInputValue(inputValue) + onInputValueChange?.(inputValue, reason) + }, + [setInputValue, onInputValueChange], + ) + + return ( + + + + {children} + {!open && + + + ) + }, +) +Combobox.displayName = "Combobox" + +export const ComboboxTagGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const { currentTabStopId, onCurrentTabStopIdChange, tagGroupRef, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const composedRefs = useComposedRefs(ref, tagGroupRef) + + return ( + onCurrentTabStopIdChange(null)} + {...props} + /> + ) +}) +ComboboxTagGroup.displayName = "ComboboxTagGroup" + +export interface ComboboxTagGroupItemProps + extends React.ComponentPropsWithoutRef { + value: string + disabled?: boolean +} + +const ComboboxTagGroupItemContext = React.createContext>({ + value: "", + disabled: false, +}) + +const useComboboxTagGroupItemContext = () => React.useContext(ComboboxTagGroupItemContext) + +export const ComboboxTagGroupItem = React.forwardRef< + React.ElementRef, + ComboboxTagGroupItemProps +>(({ onClick, onKeyDown, value: valueProp, disabled, ...props }, ref) => { + const { value, onValueChange, inputRef, currentTabStopId, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const lastItemValue = value.at(-1) + + return ( + + { + if (event.key === "Escape") { + inputRef.current?.focus() + } + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault() + inputRef.current?.focus() + } + if (event.key === "ArrowRight" && currentTabStopId === lastItemValue) { + inputRef.current?.focus() + } + if (event.key === "Backspace" || event.key === "Delete") { + onValueChange(value.filter((v) => v !== currentTabStopId)) + inputRef.current?.focus() + } + })} + onClick={composeEventHandlers(onClick, () => disabled && inputRef.current?.focus())} + tabStopId={valueProp} + focusable={!disabled} + data-disabled={disabled} + active={valueProp === lastItemValue} + {...props} + /> + + ) +}) +ComboboxTagGroupItem.displayName = "ComboboxTagGroupItem" + +export const ComboboxTagGroupItemRemove = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onClick, ...props }, ref) => { + const { value, onValueChange, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const { value: valueProp, disabled } = useComboboxTagGroupItemContext() + + return ( + onValueChange(value.filter((v) => v !== valueProp)))} + {...props} + /> + ) +}) +ComboboxTagGroupItemRemove.displayName = "ComboboxTagGroupItemRemove" + +export const ComboboxInput = React.forwardRef< + React.ElementRef, + Omit, "value" | "onValueChange"> +>(({ onKeyDown, onMouseDown, onFocus, onBlur, ...props }, ref) => { + const { + type, + inputValue, + onInputValueChange, + onInputBlur, + open, + onOpenChange, + value, + onValueChange, + inputRef, + disabled, + required, + tagGroupRef, + } = useComboboxContext() + + const composedRefs = useComposedRefs(ref, inputRef) + + return ( + { + if (!open) { + onOpenChange(true) + } + // Schedule input value change to the next tick. + setTimeout(() => onInputValueChange(search, "inputChange")) + if (!search && type === "single") { + onValueChange("") + } + }} + onKeyDown={composeEventHandlers(onKeyDown, (event) => { + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + if (!open) { + event.preventDefault() + onOpenChange(true) + } + } + if (type !== "multiple") { + return + } + if (event.key === "ArrowLeft" && !inputValue && value.length) { + tagGroupRef.current?.focus() + } + if (event.key === "Backspace" && !inputValue) { + onValueChange(value.slice(0, -1)) + } + })} + onMouseDown={composeEventHandlers(onMouseDown, () => onOpenChange(!!inputValue || !open))} + onFocus={composeEventHandlers(onFocus, () => onOpenChange(true))} + onBlur={composeEventHandlers(onBlur, (event) => { + if (!event.relatedTarget?.hasAttribute("cmdk-list")) { + onInputBlur?.(event) + } + })} + {...props} + /> + ) +}) +ComboboxInput.displayName = "ComboboxInput" + +export const ComboboxClear = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onClick, ...props }, ref) => { + const { value, onValueChange, inputValue, onInputValueChange, type } = useComboboxContext() + + const isValueEmpty = type === "single" ? !value : !value.length + + return ( + { + if (type === "single") { + onValueChange("") + } else { + onValueChange([]) + } + onInputValueChange("", "clearClick") + })} + {...props} + /> + ) +}) +ComboboxClear.displayName = "ComboboxClear" + +export const ComboboxTrigger = PopoverPrimitive.Trigger + +export const ComboboxAnchor = PopoverPrimitive.Anchor + +export const ComboboxPortal = PopoverPrimitive.Portal + +export const ComboboxContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, onOpenAutoFocus, onInteractOutside, ...props }, ref) => ( + event.preventDefault())} + onCloseAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())} + onInteractOutside={composeEventHandlers(onInteractOutside, (event) => { + if (event.target instanceof Element && event.target.hasAttribute("cmdk-input")) { + event.preventDefault() + } + })} + {...props}> + {children} + +)) +ComboboxContent.displayName = "ComboboxContent" + +export const ComboboxEmpty = CommandPrimitive.Empty + +export const ComboboxLoading = CommandPrimitive.Loading + +export interface ComboboxItemProps extends Omit, "value"> { + value: string +} + +const ComboboxItemContext = React.createContext({ isSelected: false }) + +const useComboboxItemContext = () => React.useContext(ComboboxItemContext) + +const findComboboxItemText = (children: React.ReactNode) => { + let text = "" + + React.Children.forEach(children, (child) => { + if (text) { + return + } + + if (React.isValidElement<{ children: React.ReactNode }>(child)) { + if (child.type === ComboboxItemText) { + text = child.props.children as string + } else { + text = findComboboxItemText(child.props.children) + } + } + }) + + return text +} + +export const ComboboxItem = React.forwardRef, ComboboxItemProps>( + ({ value: valueProp, children, onMouseDown, ...props }, ref) => { + const { type, value, onValueChange, onInputValueChange, onOpenChange } = useComboboxContext() + + const inputValue = React.useMemo(() => findComboboxItemText(children), [children]) + + const isSelected = type === "single" ? value === valueProp : value.includes(valueProp) + + return ( + + event.preventDefault())} + onSelect={() => { + if (type === "multiple") { + onValueChange( + value.includes(valueProp) + ? value.filter((v) => v !== valueProp) + : [...value, valueProp], + ) + onInputValueChange("", "itemSelect") + } else { + onValueChange(valueProp) + onInputValueChange(inputValue, "itemSelect") + // Schedule open change to the next tick. + setTimeout(() => onOpenChange(false)) + } + }} + value={inputValue} + {...props}> + {children} + + + ) + }, +) +ComboboxItem.displayName = "ComboboxItem" + +export const ComboboxItemIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const { isSelected } = useComboboxItemContext() + + if (!isSelected) { + return null + } + + return +}) +ComboboxItemIndicator.displayName = "ComboboxItemIndicator" + +export interface ComboboxItemTextProps extends React.ComponentPropsWithoutRef { + children: string +} + +export const ComboboxItemText = (props: ComboboxItemTextProps) => +ComboboxItemText.displayName = "ComboboxItemText" + +export const ComboboxGroup = CommandPrimitive.Group + +export const ComboboxSeparator = CommandPrimitive.Separator + +const Root = Combobox +const TagGroup = ComboboxTagGroup +const TagGroupItem = ComboboxTagGroupItem +const TagGroupItemRemove = ComboboxTagGroupItemRemove +const Input = ComboboxInput +const Clear = ComboboxClear +const Trigger = ComboboxTrigger +const Anchor = ComboboxAnchor +const Portal = ComboboxPortal +const Content = ComboboxContent +const Empty = ComboboxEmpty +const Loading = ComboboxLoading +const Item = ComboboxItem +const ItemIndicator = ComboboxItemIndicator +const ItemText = ComboboxItemText +const Group = ComboboxGroup +const Separator = ComboboxSeparator + +export { + Root, + TagGroup, + TagGroupItem, + TagGroupItemRemove, + Input, + Clear, + Trigger, + Anchor, + Portal, + Content, + Empty, + Loading, + Item, + ItemIndicator, + ItemText, + Group, + Separator, +} diff --git a/webview-ui/src/components/ui/combobox.tsx b/webview-ui/src/components/ui/combobox.tsx new file mode 100644 index 0000000000..24b2f7be1f --- /dev/null +++ b/webview-ui/src/components/ui/combobox.tsx @@ -0,0 +1,177 @@ +"use client" + +import * as React from "react" +import { Slottable } from "@radix-ui/react-slot" +import { cva } from "class-variance-authority" +import { Check, ChevronsUpDown, Loader, X } from "lucide-react" + +import { cn } from "@/lib/utils" +import * as ComboboxPrimitive from "@/components/ui/combobox-primitive" +import { badgeVariants } from "@/components/ui/badge" +// import * as ComboboxPrimitive from "@/registry/default/ui/combobox-primitive" +import { + InputBase, + InputBaseAdornmentButton, + InputBaseControl, + InputBaseFlexWrapper, + InputBaseInput, +} from "@/components/ui/input-base" + +export const Combobox = ComboboxPrimitive.Root + +const ComboboxInputBase = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, ...props }, ref) => ( + + + {children} + + + + + + + + + + + + +)) +ComboboxInputBase.displayName = "ComboboxInputBase" + +export const ComboboxInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + + + + + + + +)) +ComboboxInput.displayName = "ComboboxInput" + +export const ComboboxTagsInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, ...props }, ref) => ( + + + + {children} + + + + + + + + +)) +ComboboxTagsInput.displayName = "ComboboxTagsInput" + +export const ComboboxTag = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, className, ...props }, ref) => ( + + {children} + + + Remove + + +)) +ComboboxTag.displayName = "ComboboxTag" + +export const ComboboxContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "start", alignOffset = 0, ...props }, ref) => ( + + + +)) +ComboboxContent.displayName = "ComboboxContent" + +export const ComboboxEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxEmpty.displayName = "ComboboxEmpty" + +export const ComboboxLoading = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ComboboxLoading.displayName = "ComboboxLoading" + +export const ComboboxGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxGroup.displayName = "ComboboxGroup" + +const ComboboxSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxSeparator.displayName = "ComboboxSeparator" + +export const comboboxItemStyle = cva( + "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-vscode-dropdown-foreground data-[disabled=true]:opacity-50", +) + +export const ComboboxItem = React.forwardRef< + React.ElementRef, + Omit, "children"> & + Pick, "children"> +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +ComboboxItem.displayName = "ComboboxItem" diff --git a/webview-ui/src/components/ui/input-base.tsx b/webview-ui/src/components/ui/input-base.tsx new file mode 100644 index 0000000000..9dbda6eb13 --- /dev/null +++ b/webview-ui/src/components/ui/input-base.tsx @@ -0,0 +1,157 @@ +/* eslint-disable react/jsx-no-comment-textnodes */ +/* eslint-disable react/jsx-pascal-case */ +"use client" + +import * as React from "react" +import { composeEventHandlers } from "@radix-ui/primitive" +import { composeRefs } from "@radix-ui/react-compose-refs" +import { Primitive } from "@radix-ui/react-primitive" +import { Slot } from "@radix-ui/react-slot" + +import { cn } from "@/lib/utils" +import { Button } from "./button" + +export type InputBaseContextProps = Pick & { + controlRef: React.RefObject + onFocusedChange: (focused: boolean) => void +} + +const InputBaseContext = React.createContext({ + autoFocus: false, + controlRef: { current: null }, + disabled: false, + onFocusedChange: () => {}, +}) + +const useInputBaseContext = () => React.useContext(InputBaseContext) + +export interface InputBaseProps extends React.ComponentPropsWithoutRef { + autoFocus?: boolean + disabled?: boolean +} + +export const InputBase = React.forwardRef, InputBaseProps>( + ({ autoFocus, disabled, className, onClick, ...props }, ref) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [focused, setFocused] = React.useState(false) + + const controlRef = React.useRef(null) + + return ( + + { + // Based on MUI's implementation. + // https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460 + if (controlRef.current && event.currentTarget === event.target) { + controlRef.current.focus() + } + })} + className={cn( + "flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-0.5 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50", + disabled && "cursor-not-allowed opacity-50", + className, + )} + {...props} + /> + + ) + }, +) +InputBase.displayName = "InputBase" + +export const InputBaseFlexWrapper = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +InputBaseFlexWrapper.displayName = "InputBaseFlexWrapper" + +export const InputBaseControl = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onFocus, onBlur, ...props }, ref) => { + const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBaseContext() + + return ( + onFocusedChange(true))} + onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))} + {...{ disabled }} + {...props} + /> + ) +}) +InputBaseControl.displayName = "InputBaseControl" + +export interface InputBaseAdornmentProps extends React.ComponentPropsWithoutRef<"div"> { + asChild?: boolean + disablePointerEvents?: boolean +} + +export const InputBaseAdornment = React.forwardRef, InputBaseAdornmentProps>( + ({ className, disablePointerEvents, asChild, children, ...props }, ref) => { + const Comp = asChild ? Slot : typeof children === "string" ? "p" : "div" + + const isAction = React.isValidElement(children) && children.type === InputBaseAdornmentButton + + return ( + + {children} + + ) + }, +) +InputBaseAdornment.displayName = "InputBaseAdornment" + +export const InputBaseAdornmentButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ type = "button", variant = "ghost", size = "icon", disabled: disabledProp, className, ...props }, ref) => { + const { disabled } = useInputBaseContext() + + return ( + - {item?.size && ( + {!!item?.size && item.size > 0 && ( - - - - - - No model found. - - {modelIds.map((model) => ( - - {model} - - - ))} - - {allowCustomModel && ( - - { - setIsCustomModel(true) - setOpen(false) - }}> - + Add custom model - - - )} - - - - + + + + No model found. + {modelIds.map((model) => ( + + {model} + + ))} + + + {selectedModelId && selectedModelInfo && ( onSelect(recommendedModel)}>{recommendedModel}. You can also try searching "free" for no-cost options currently available.

- {allowCustomModel && isCustomModel && ( -
-
-

Add Custom Model

- setCustomModelId(e.target.value)} - /> -
- - -
-
-
- )} ) } diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx deleted file mode 100644 index 040da1d421..0000000000 --- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react" -import { useExtensionState } from "../../context/ExtensionStateContext" -import { ModelPicker } from "./ModelPicker" - -const OpenAiModelPicker: React.FC = () => { - const { apiConfiguration } = useExtensionState() - - return ( - - ) -} - -export default OpenAiModelPicker diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx deleted file mode 100644 index c773478e54..0000000000 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { openRouterDefaultModelId } from "../../../../src/shared/api" - -export const OpenRouterModelPicker = () => ( - -) diff --git a/webview-ui/src/components/settings/RequestyModelPicker.tsx b/webview-ui/src/components/settings/RequestyModelPicker.tsx deleted file mode 100644 index c65067068a..0000000000 --- a/webview-ui/src/components/settings/RequestyModelPicker.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { requestyDefaultModelId } from "../../../../src/shared/api" -import { useExtensionState } from "@/context/ExtensionStateContext" - -export const RequestyModelPicker = () => { - const { apiConfiguration } = useExtensionState() - return ( - - ) -} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 761e856521..75ba11107c 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,4 +1,4 @@ -import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react" +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { Dropdown, type DropdownOption } from "vscrui" @@ -45,7 +45,6 @@ const SettingsView = forwardRef(({ onDone }, // TODO: Reduce WebviewMessage/ExtensionState complexity const { currentApiConfigName } = extensionState const { - apiConfiguration, alwaysAllowReadOnly, allowedCommands, alwaysAllowBrowser, @@ -69,6 +68,9 @@ const SettingsView = forwardRef(({ onDone }, terminalOutputLineLimit, writeDelayMs, } = cachedState + + //Make sure apiConfiguration is initialized and managed by SettingsView + const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) useEffect(() => { // Update only when currentApiConfigName is changed diff --git a/webview-ui/src/components/settings/UnboundModelPicker.tsx b/webview-ui/src/components/settings/UnboundModelPicker.tsx deleted file mode 100644 index 4901884f1e..0000000000 --- a/webview-ui/src/components/settings/UnboundModelPicker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { unboundDefaultModelId } from "../../../../src/shared/api" - -export const UnboundModelPicker = () => ( - -) diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx index 4e7c67c187..49d60c55c4 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx @@ -3,7 +3,6 @@ import { screen, fireEvent, render } from "@testing-library/react" import { act } from "react" import { ModelPicker } from "../ModelPicker" -import { useExtensionState } from "../../../context/ExtensionStateContext" jest.mock("../../../context/ExtensionStateContext", () => ({ useExtensionState: jest.fn(), @@ -20,36 +19,40 @@ global.ResizeObserver = MockResizeObserver Element.prototype.scrollIntoView = jest.fn() describe("ModelPicker", () => { - const mockOnUpdateApiConfig = jest.fn() - const mockSetApiConfiguration = jest.fn() - + const mockSetApiConfigurationField = jest.fn() + const modelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + } + const mockModels = { + model1: { name: "Model 1", description: "Test model 1", ...modelInfo }, + model2: { name: "Model 2", description: "Test model 2", ...modelInfo }, + } const defaultProps = { + apiConfiguration: {}, defaultModelId: "model1", - modelsKey: "glamaModels" as const, - configKey: "glamaModelId" as const, - infoKey: "glamaModelInfo" as const, - refreshMessageType: "refreshGlamaModels" as const, + defaultModelInfo: modelInfo, + modelIdKey: "glamaModelId" as const, + modelInfoKey: "glamaModelInfo" as const, serviceName: "Test Service", serviceUrl: "https://test.service", recommendedModel: "recommended-model", - } - - const mockModels = { - model1: { name: "Model 1", description: "Test model 1" }, - model2: { name: "Model 2", description: "Test model 2" }, + models: mockModels, + setApiConfigurationField: mockSetApiConfigurationField, } beforeEach(() => { jest.clearAllMocks() - ;(useExtensionState as jest.Mock).mockReturnValue({ - apiConfiguration: {}, - setApiConfiguration: mockSetApiConfiguration, - glamaModels: mockModels, - onUpdateApiConfig: mockOnUpdateApiConfig, - }) }) - it("calls onUpdateApiConfig when a model is selected", async () => { + it("calls setApiConfigurationField when a model is selected", async () => { await act(async () => { render() }) @@ -67,20 +70,12 @@ describe("ModelPicker", () => { await act(async () => { // Find and click the model item by its value. - const modelItem = screen.getByRole("option", { name: "model2" }) - fireEvent.click(modelItem) + const modelItem = screen.getByTestId("model-input") + fireEvent.input(modelItem, { target: { value: "model2" } }) }) // Verify the API config was updated. - expect(mockSetApiConfiguration).toHaveBeenCalledWith({ - glamaModelId: "model2", - glamaModelInfo: mockModels["model2"], - }) - - // Verify onUpdateApiConfig was called with the new config. - expect(mockOnUpdateApiConfig).toHaveBeenCalledWith({ - glamaModelId: "model2", - glamaModelInfo: mockModels["model2"], - }) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelIdKey, "model2") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelInfoKey, mockModels.model2) }) }) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 19b13e2c6c..97c702637c 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,9 +1,4 @@ -import { - ApiConfiguration, - glamaDefaultModelId, - openRouterDefaultModelId, - unboundDefaultModelId, -} from "../../../src/shared/api" +import { ApiConfiguration } from "../../../src/shared/api" import { ModelInfo } from "../../../src/shared/api" export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { if (apiConfiguration) { @@ -86,7 +81,7 @@ export function validateModelId( if (apiConfiguration) { switch (apiConfiguration.apiProvider) { case "glama": - const glamaModelId = apiConfiguration.glamaModelId || glamaDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default + const glamaModelId = apiConfiguration.glamaModelId if (!glamaModelId) { return "You must provide a model ID." } @@ -96,7 +91,7 @@ export function validateModelId( } break case "openrouter": - const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default + const modelId = apiConfiguration.openRouterModelId if (!modelId) { return "You must provide a model ID." } @@ -106,7 +101,7 @@ export function validateModelId( } break case "unbound": - const unboundModelId = apiConfiguration.unboundModelId || unboundDefaultModelId + const unboundModelId = apiConfiguration.unboundModelId if (!unboundModelId) { return "You must provide a model ID." } From 1a3b8700ba2c8f93f31cdfed7aa0fadb38d584d0 Mon Sep 17 00:00:00 2001 From: System233 Date: Wed, 26 Feb 2025 06:42:29 +0800 Subject: [PATCH 22/32] Improved error message feedback in settings panel --- webview-ui/src/__mocks__/vscrui.ts | 3 + .../components/settings/ApiErrorMessage.tsx | 16 +++++ .../src/components/settings/ApiOptions.tsx | 69 ++++++++----------- .../src/components/settings/ModelPicker.tsx | 41 ++++++++--- .../src/components/settings/SettingsView.tsx | 53 ++++---------- .../settings/__tests__/ApiOptions.test.tsx | 4 ++ .../src/components/welcome/WelcomeView.tsx | 2 + 7 files changed, 98 insertions(+), 90 deletions(-) create mode 100644 webview-ui/src/components/settings/ApiErrorMessage.tsx diff --git a/webview-ui/src/__mocks__/vscrui.ts b/webview-ui/src/__mocks__/vscrui.ts index 76760ba5cc..9b4a20f4d6 100644 --- a/webview-ui/src/__mocks__/vscrui.ts +++ b/webview-ui/src/__mocks__/vscrui.ts @@ -8,6 +8,9 @@ export const Dropdown = ({ children, value, onChange }: any) => export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children) +export const Button = ({ children, ...props }: any) => + React.createElement("div", { "data-testid": "mock-button", ...props }, children) + export type DropdownOption = { label: string value: string diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx new file mode 100644 index 0000000000..4b419957b6 --- /dev/null +++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx @@ -0,0 +1,16 @@ +import React from "react" + +interface ApiErrorMessageProps { + errorMessage: string | undefined + children?: React.ReactNode +} +const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => { + return ( +
+ + {errorMessage} + {children} +
+ ) +} +export default ApiErrorMessage diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 594cd2fd5f..f0c2b0e45f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useMemo, useState } from "react" +import React, { memo, useCallback, useEffect, useMemo, useState } from "react" import { useDebounce, useEvent } from "react-use" import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" @@ -42,23 +42,25 @@ import { ModelInfoView } from "./ModelInfoView" import { DROPDOWN_Z_INDEX } from "./styles" import { ModelPicker } from "./ModelPicker" import { TemperatureControl } from "./TemperatureControl" +import { validateApiConfiguration, validateModelId } from "@/utils/validate" +import ApiErrorMessage from "./ApiErrorMessage" interface ApiOptionsProps { uriScheme: string | undefined apiConfiguration: ApiConfiguration setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void - apiErrorMessage?: string - modelIdErrorMessage?: string fromWelcomeView?: boolean + errorMessage: string | undefined + setErrorMessage: React.Dispatch> } const ApiOptions = ({ uriScheme, apiConfiguration, setApiConfigurationField, - apiErrorMessage, - modelIdErrorMessage, fromWelcomeView, + errorMessage, + setErrorMessage, }: ApiOptionsProps) => { const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) @@ -146,6 +148,13 @@ const ApiOptions = ({ ], ) + useEffect(() => { + const apiValidationResult = + validateApiConfiguration(apiConfiguration) || + validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels) + setErrorMessage(apiValidationResult) + }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels]) + const handleMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data switch (message.type) { @@ -626,6 +635,7 @@ const ApiOptions = ({ ]} /> + {errorMessage && }

{/* end Model Info Configuration */} - -

- - (Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected.) - -

)} @@ -1100,6 +1099,7 @@ const ApiOptions = ({ placeholder={"e.g. meta-llama-3.1-8b-instruct"}> Model ID + {errorMessage && } {lmStudioModels.length > 0 && ( Model ID + {errorMessage && ( +
+ + {errorMessage} +
+ )} {ollamaModels.length > 0 && ( )} - {apiErrorMessage && ( -

- - {apiErrorMessage} -

- )} - {selectedProvider === "glama" && ( )} @@ -1364,6 +1360,7 @@ const ApiOptions = ({ serviceName="OpenRouter" serviceUrl="https://openrouter.ai/models" recommendedModel="anthropic/claude-3.7-sonnet" + errorMessage={errorMessage} /> )} {selectedProvider === "requesty" && ( @@ -1378,6 +1375,7 @@ const ApiOptions = ({ serviceName="Requesty" serviceUrl="https://requesty.ai" recommendedModel="anthropic/claude-3-7-sonnet-latest" + errorMessage={errorMessage} /> )} @@ -1401,6 +1399,7 @@ const ApiOptions = ({ {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} {selectedProvider === "mistral" && createDropdown(mistralModels)} + {errorMessage && } )} - - {modelIdErrorMessage && ( -

- - {modelIdErrorMessage} -

- )} ) } diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 8fd6d82daa..fd62bfb97b 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -5,6 +5,7 @@ import { normalizeApiConfiguration } from "./ApiOptions" import { ModelInfoView } from "./ModelInfoView" import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox" +import ApiErrorMessage from "./ApiErrorMessage" type ExtractType = NonNullable< { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration] @@ -30,6 +31,7 @@ interface ModelPickerProps { apiConfiguration: ApiConfiguration setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void defaultModelInfo?: ModelInfo + errorMessage?: string } export const ModelPicker = ({ @@ -43,6 +45,7 @@ export const ModelPicker = ({ apiConfiguration, setApiConfigurationField, defaultModelInfo, + errorMessage, }: ModelPickerProps) => { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) @@ -69,11 +72,16 @@ export const ModelPicker = ({ return ( <>
Model
- + No model found. @@ -85,13 +93,30 @@ export const ModelPicker = ({ - {selectedModelId && selectedModelInfo && ( - + {errorMessage ? ( + +

+ + Note: Roo Code uses complex prompts and works best + with Claude models. Less capable models may not work as expected. + +

+
+ ) : ( + selectedModelId && + selectedModelInfo && ( + + ) )}

The extension automatically fetches the latest list of models available on{" "} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 75ba11107c..ee032c3ee0 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,6 +1,6 @@ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { Dropdown, type DropdownOption } from "vscrui" +import { Button, Dropdown, type DropdownOption } from "vscrui" import { AlertDialog, @@ -14,7 +14,6 @@ import { } from "@/components/ui" import { vscode } from "../../utils/vscode" -import { validateApiConfiguration, validateModelId } from "../../utils/validate" import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext" import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" import { ApiConfiguration } from "../../../../src/shared/api" @@ -33,14 +32,13 @@ export interface SettingsViewRef { const SettingsView = forwardRef(({ onDone }, ref) => { const extensionState = useExtensionState() - const [apiErrorMessage, setApiErrorMessage] = useState(undefined) - const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) const [commandInput, setCommandInput] = useState("") const [isDiscardDialogShow, setDiscardDialogShow] = useState(false) const [cachedState, setCachedState] = useState(extensionState) const [isChangeDetected, setChangeDetected] = useState(false) const prevApiConfigName = useRef(extensionState.currentApiConfigName) const confirmDialogHandler = useRef<() => void>() + const [errorMessage, setErrorMessage] = useState(undefined) // TODO: Reduce WebviewMessage/ExtensionState complexity const { currentApiConfigName } = extensionState @@ -135,20 +133,9 @@ const SettingsView = forwardRef(({ onDone }, } }) }, []) - + const isSettingValid = !errorMessage const handleSubmit = () => { - const apiValidationResult = validateApiConfiguration(apiConfiguration) - - const modelIdValidationResult = validateModelId( - apiConfiguration, - extensionState.glamaModels, - extensionState.openRouterModels, - ) - - setApiErrorMessage(apiValidationResult) - setModelIdErrorMessage(modelIdValidationResult) - - if (!apiValidationResult && !modelIdValidationResult) { + if (isSettingValid) { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) @@ -177,23 +164,6 @@ const SettingsView = forwardRef(({ onDone }, } } - useEffect(() => { - setApiErrorMessage(undefined) - setModelIdErrorMessage(undefined) - }, [apiConfiguration]) - - // Initial validation on mount - useEffect(() => { - const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId( - apiConfiguration, - extensionState.glamaModels, - extensionState.openRouterModels, - ) - setApiErrorMessage(apiValidationResult) - setModelIdErrorMessage(modelIdValidationResult) - }, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels]) - const checkUnsaveChanges = useCallback( (then: () => void) => { if (isChangeDetected) { @@ -287,13 +257,14 @@ const SettingsView = forwardRef(({ onDone }, justifyContent: "space-between", gap: "6px", }}> - + disabled={!isChangeDetected || !isSettingValid}> Save - + (({ onDone }, uriScheme={extensionState.uriScheme} apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} - apiErrorMessage={apiErrorMessage} - modelIdErrorMessage={modelIdErrorMessage} + errorMessage={errorMessage} + setErrorMessage={setErrorMessage} /> diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index 8f2d0dff89..73394bae10 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -51,6 +51,8 @@ describe("ApiOptions", () => { render( {}} uriScheme={undefined} apiConfiguration={{}} setApiConfigurationField={() => {}} @@ -69,4 +71,6 @@ describe("ApiOptions", () => { renderApiOptions({ fromWelcomeView: true }) expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument() }) + + //TODO: More test cases needed }) diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 858d2622f3..5d880efc0b 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -42,6 +42,8 @@ const WelcomeView = () => { apiConfiguration={apiConfiguration || {}} uriScheme={uriScheme} setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })} + errorMessage={errorMessage} + setErrorMessage={setErrorMessage} /> From 48975003afe593c975476acfd348ceb6110d7ced Mon Sep 17 00:00:00 2001 From: System233 Date: Wed, 26 Feb 2025 06:50:11 +0800 Subject: [PATCH 23/32] Remove ModelInfo related exports from ExtensionStateContext --- .../src/context/ExtensionStateContext.tsx | 84 +------------------ 1 file changed, 1 insertion(+), 83 deletions(-) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3dca8d5f51..c2c4d181e4 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -1,18 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react" import { useEvent } from "react-use" import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage" -import { - ApiConfiguration, - ModelInfo, - glamaDefaultModelId, - glamaDefaultModelInfo, - openRouterDefaultModelId, - openRouterDefaultModelInfo, - unboundDefaultModelId, - unboundDefaultModelInfo, - requestyDefaultModelId, - requestyDefaultModelInfo, -} from "../../../src/shared/api" +import { ApiConfiguration } from "../../../src/shared/api" import { vscode } from "../utils/vscode" import { convertTextMateToHljs } from "../utils/textMateToHljs" import { findLastIndex } from "../../../src/shared/array" @@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean showWelcome: boolean theme: any - glamaModels: Record - requestyModels: Record - openRouterModels: Record - unboundModels: Record - openAiModels: string[] mcpServers: McpServer[] currentCheckpoint?: string filePaths: string[] @@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState { setRateLimitSeconds: (value: number) => void setCurrentApiConfigName: (value: string) => void setListApiConfigMeta: (value: ApiConfigMeta[]) => void - onUpdateApiConfig: (apiConfig: ApiConfiguration) => void mode: Mode setMode: (value: Mode) => void setCustomModePrompts: (value: CustomModePrompts) => void @@ -124,21 +107,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [showWelcome, setShowWelcome] = useState(false) const [theme, setTheme] = useState(undefined) const [filePaths, setFilePaths] = useState([]) - const [glamaModels, setGlamaModels] = useState>({ - [glamaDefaultModelId]: glamaDefaultModelInfo, - }) const [openedTabs, setOpenedTabs] = useState>([]) - const [openRouterModels, setOpenRouterModels] = useState>({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, - }) - const [unboundModels, setUnboundModels] = useState>({ - [unboundDefaultModelId]: unboundDefaultModelInfo, - }) - const [requestyModels, setRequestyModels] = useState>({ - [requestyDefaultModelId]: requestyDefaultModelInfo, - }) - const [openAiModels, setOpenAiModels] = useState([]) const [mcpServers, setMcpServers] = useState([]) const [currentCheckpoint, setCurrentCheckpoint] = useState() @@ -146,18 +116,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [], ) - - const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => { - setState((currentState) => { - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentState.currentApiConfigName, - apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig }, - }) - return currentState // No state update needed - }) - }, []) - const handleMessage = useCallback( (event: MessageEvent) => { const message: ExtensionMessage = event.data @@ -202,40 +160,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }) break } - case "glamaModels": { - const updatedModels = message.glamaModels ?? {} - setGlamaModels({ - [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openRouterModels": { - const updatedModels = message.openRouterModels ?? {} - setOpenRouterModels({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openAiModels": { - const updatedModels = message.openAiModels ?? [] - setOpenAiModels(updatedModels) - break - } - case "unboundModels": { - const updatedModels = message.unboundModels ?? {} - setUnboundModels(updatedModels) - break - } - case "requestyModels": { - const updatedModels = message.requestyModels ?? {} - setRequestyModels({ - [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } case "mcpServers": { setMcpServers(message.mcpServers ?? []) break @@ -264,11 +188,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode didHydrateState, showWelcome, theme, - glamaModels, - requestyModels, - openRouterModels, - openAiModels, - unboundModels, mcpServers, currentCheckpoint, filePaths, @@ -316,7 +235,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), setListApiConfigMeta, - onUpdateApiConfig, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })), setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })), From 05151ed2254e8698d4c380ceb8daa36e019f82aa Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 25 Feb 2025 18:30:37 -0500 Subject: [PATCH 24/32] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84bec2645a..28045436e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "roo-cline", "displayName": "Roo Code (prev. Roo Cline)", - "description": "An AI-powered autonomous coding agent that lives in your editor.", + "description": "A whole dev team of AI agents in your editor.", "publisher": "RooVeterinaryInc", "version": "3.7.4", "icon": "assets/icons/rocket.png", From e56908f6c1b800bd2a2b4edd85f725c1a0055920 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 15:48:20 -0800 Subject: [PATCH 25/32] Thinking settings tweaks --- src/api/providers/anthropic.ts | 23 +++++---- src/api/providers/openrouter.ts | 12 ++++- src/shared/api.ts | 14 +++++- .../src/components/settings/ApiOptions.tsx | 50 ++++++------------- 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 2d1f07f833..c907350607 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -14,8 +14,6 @@ import { ApiStream } from "../transform/stream" const ANTHROPIC_DEFAULT_TEMPERATURE = 0 -const THINKING_MODELS = ["claude-3-7-sonnet-20250219"] - export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic @@ -32,16 +30,19 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { let stream: AnthropicStream const cacheControl: CacheControlEphemeral = { type: "ephemeral" } - const modelId = this.getModel().id - const maxTokens = this.getModel().info.maxTokens || 8192 + let { id: modelId, info: modelInfo } = this.getModel() + const maxTokens = modelInfo.maxTokens || 8192 + const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192) let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE let thinking: BetaThinkingConfigParam | undefined = undefined - if (THINKING_MODELS.includes(modelId)) { - thinking = this.options.anthropicThinking - ? { type: "enabled", budget_tokens: this.options.anthropicThinking } - : { type: "disabled" } - + // Anthropic "Thinking" models require a temperature of 1.0. + if (modelId === "claude-3-7-sonnet-20250219:thinking") { + // The `:thinking` variant is a virtual identifier for the + // `claude-3-7-sonnet-20250219` model with a thinking budget. + // We can handle this more elegantly in the future. + modelId = "claude-3-7-sonnet-20250219" + thinking = { type: "enabled", budget_tokens: budgetTokens } temperature = 1.0 } @@ -114,8 +115,8 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { default: { stream = (await this.client.messages.create({ model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, - temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, + max_tokens: maxTokens, + temperature, system: [{ text: systemPrompt, type: "text" }], messages, // tools, diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 69c55b8e71..6bf4fa4a8c 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" +import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta" import axios from "axios" import OpenAI from "openai" import delay from "delay" @@ -17,6 +18,7 @@ const OPENROUTER_DEFAULT_TEMPERATURE = 0 type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { transforms?: string[] include_reasoning?: boolean + thinking?: BetaThinkingConfigParam } // Add custom interface for OpenRouter usage chunk. @@ -57,7 +59,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { // prompt caching: https://openrouter.ai/docs/prompt-caching // this is specifically for claude models (some models may 'support prompt caching' automatically without this) switch (true) { - case this.getModel().id.startsWith("anthropic/"): + case modelId.startsWith("anthropic/"): openAiMessages[0] = { role: "system", content: [ @@ -108,8 +110,13 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { let temperature = this.options.modelTemperature ?? defaultTemperature + const maxTokens = modelInfo.maxTokens + const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192) + let thinking: BetaThinkingConfigParam | undefined = undefined + // Anthropic "Thinking" models require a temperature of 1.0. if (modelInfo.thinking) { + thinking = { type: "enabled", budget_tokens: budgetTokens } temperature = 1.0 } @@ -118,8 +125,9 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { const completionParams: OpenRouterChatCompletionParams = { model: modelId, - max_tokens: modelInfo.maxTokens, + max_tokens: maxTokens, temperature, + thinking, // OpenRouter is temporarily supporting this. top_p: topP, messages: openAiMessages, stream: true, diff --git a/src/shared/api.ts b/src/shared/api.ts index 63707e52b4..5d4b8b120d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -103,7 +103,7 @@ export const THINKING_BUDGET = { export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219" export const anthropicModels = { - "claude-3-7-sonnet-20250219": { + "claude-3-7-sonnet-20250219:thinking": { maxTokens: 16384, contextWindow: 200_000, supportsImages: true, @@ -115,6 +115,18 @@ export const anthropicModels = { cacheReadsPrice: 0.3, // $0.30 per million tokens thinking: true, }, + "claude-3-7-sonnet-20250219": { + maxTokens: 16384, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million input tokens + outputPrice: 15.0, // $15 per million output tokens + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + thinking: false, + }, "claude-3-5-sonnet-20241022": { maxTokens: 8192, contextWindow: 200_000, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 9d17cae4fa..73dc4fd41f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -73,7 +73,7 @@ const ApiOptions = ({ const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const anthropicThinkingBudget = apiConfiguration?.anthropicThinking + const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default const noTransform = (value: T) => value const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any @@ -1272,39 +1272,21 @@ const ApiOptions = ({ )} {selectedModelInfo && selectedModelInfo.thinking && ( -

- - setApiConfigurationField( - "anthropicThinking", - checked - ? Math.min( - THINKING_BUDGET.default, - selectedModelInfo.maxTokens ?? THINKING_BUDGET.default, - ) - : undefined, - ) - }> - Thinking? - - {anthropicThinkingBudget && ( - <> -
- Number of tokens Claude is allowed to use for its internal reasoning process. -
-
- setApiConfigurationField("anthropicThinking", value[0])} - /> -
{anthropicThinkingBudget}
-
- - )} +
+
Thinking Budget
+
+ setApiConfigurationField("anthropicThinking", value[0])} + /> +
{anthropicThinkingBudget}
+
+
+ Number of tokens Claude is allowed to use for its internal reasoning process. +
)} From 8971e47b96ec7ae3a6de4ec5a95a2acab4cba1b7 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 15:53:32 -0800 Subject: [PATCH 26/32] Add changeset --- .changeset/swift-kings-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-kings-attack.md diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md new file mode 100644 index 0000000000..8a8a425611 --- /dev/null +++ b/.changeset/swift-kings-attack.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Pass "thinking" params to OpenRouter From 33fd3bd6b35caafce66fcd53b9070f60279fcc0d Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 16:26:40 -0800 Subject: [PATCH 27/32] Fix budgetTokens --- src/api/providers/anthropic.ts | 2 +- src/api/providers/openrouter.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index c907350607..ad58a1cf6b 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -32,7 +32,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { id: modelId, info: modelInfo } = this.getModel() const maxTokens = modelInfo.maxTokens || 8192 - const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192) let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE let thinking: BetaThinkingConfigParam | undefined = undefined @@ -42,6 +41,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { // `claude-3-7-sonnet-20250219` model with a thinking budget. // We can handle this more elegantly in the future. modelId = "claude-3-7-sonnet-20250219" + const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024) thinking = { type: "enabled", budget_tokens: budgetTokens } temperature = 1.0 } diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6bf4fa4a8c..0a9488e816 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -109,13 +109,11 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { } let temperature = this.options.modelTemperature ?? defaultTemperature - - const maxTokens = modelInfo.maxTokens - const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192) let thinking: BetaThinkingConfigParam | undefined = undefined - // Anthropic "Thinking" models require a temperature of 1.0. if (modelInfo.thinking) { + const maxTokens = modelInfo.maxTokens || 8192 + const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024) thinking = { type: "enabled", budget_tokens: budgetTokens } temperature = 1.0 } @@ -125,7 +123,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { const completionParams: OpenRouterChatCompletionParams = { model: modelId, - max_tokens: maxTokens, + max_tokens: modelInfo.maxTokens, temperature, thinking, // OpenRouter is temporarily supporting this. top_p: topP, From f3d02030ac47420d8a9735c0d02f70e561475dae Mon Sep 17 00:00:00 2001 From: System233 Date: Wed, 26 Feb 2025 08:37:28 +0800 Subject: [PATCH 28/32] Fix: Input/output prices should be parsed using parseFloat --- webview-ui/src/components/settings/ApiOptions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 73dc4fd41f..bfcf93256e 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -900,7 +900,7 @@ const ApiOptions = ({ }} onChange={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value - const parsed = parseInt(value) + const parsed = parseFloat(value) return { ...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults), @@ -945,7 +945,7 @@ const ApiOptions = ({ }} onChange={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value - const parsed = parseInt(value) + const parsed = parseFloat(value) return { ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), From 41e75bc9890036674cd31ce2d9da23fcd5127956 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 23:02:43 -0800 Subject: [PATCH 29/32] Model picker fixes --- .changeset/real-ties-destroy.md | 5 + src/api/providers/requesty.ts | 36 +- src/core/webview/ClineProvider.ts | 22 +- src/shared/ExtensionMessage.ts | 13 +- src/shared/WebviewMessage.ts | 5 +- webview-ui/package-lock.json | 2 + .../components/settings/ApiErrorMessage.tsx | 18 +- .../src/components/settings/ApiOptions.tsx | 445 ++++++++---------- .../src/components/settings/ModelPicker.tsx | 91 ++-- .../src/components/settings/SettingsView.tsx | 48 +- .../components/settings/ThinkingBudget.tsx | 29 ++ webview-ui/src/components/ui/alert-dialog.tsx | 151 +++--- webview-ui/src/components/ui/dialog.tsx | 158 ++++--- .../src/components/welcome/WelcomeView.tsx | 8 +- webview-ui/src/utils/validate.ts | 254 ++++++---- 15 files changed, 664 insertions(+), 621 deletions(-) create mode 100644 .changeset/real-ties-destroy.md create mode 100644 webview-ui/src/components/settings/ThinkingBudget.tsx diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md new file mode 100644 index 0000000000..a2e9ba8eb0 --- /dev/null +++ b/.changeset/real-ties-destroy.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix model picker diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 2151a7172d..5e570ca2a2 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler { } } -export async function getRequestyModels({ apiKey }: { apiKey?: string }) { +export async function getRequestyModels() { const models: Record = {} - if (!apiKey) { - return models - } - try { - const config: Record = {} - config["headers"] = { Authorization: `Bearer ${apiKey}` } - - const response = await axios.get("https://router.requesty.ai/v1/models", config) + const response = await axios.get("https://router.requesty.ai/v1/models") const rawModels = response.data.data for (const rawModel of rawModels) { + // { + // id: "anthropic/claude-3-5-sonnet-20240620", + // object: "model", + // created: 1740552655, + // owned_by: "system", + // input_price: 0.0000028, + // caching_price: 0.00000375, + // cached_price: 3e-7, + // output_price: 0.000015, + // max_output_tokens: 8192, + // context_window: 200000, + // supports_caching: true, + // description: + // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.", + // } + const modelInfo: ModelInfo = { maxTokens: rawModel.max_output_tokens, contextWindow: rawModel.context_window, - supportsImages: rawModel.support_image, - supportsComputerUse: rawModel.support_computer_use, supportsPromptCache: rawModel.supports_caching, inputPrice: parseApiPrice(rawModel.input_price), outputPrice: parseApiPrice(rawModel.output_price), @@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) { switch (rawModel.id) { case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true modelInfo.maxTokens = 16384 break + case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true + modelInfo.maxTokens = 8192 + break case rawModel.id.startsWith("anthropic/"): modelInfo.maxTokens = 8192 break diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 118bbddcf5..bc6f457868 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } }) - const requestyApiKey = await this.getSecret("requestyApiKey") - - getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => { + getRequestyModels().then(async (requestyModels) => { if (Object.keys(requestyModels).length > 0) { await fs.writeFile( path.join(cacheDir, GlobalFileNames.requestyModels), @@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "refreshRequestyModels": - if (message?.values?.apiKey) { - const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey }) + const requestyModels = await getRequestyModels() - if (Object.keys(requestyModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.requestyModels), - JSON.stringify(requestyModels), - ) - await this.postMessageToWebview({ type: "requestyModels", requestyModels }) - } + if (Object.keys(requestyModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.requestyModels), + JSON.stringify(requestyModels), + ) + await this.postMessageToWebview({ type: "requestyModels", requestyModels }) } break diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 8f64a9ba05..e87edffed1 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -27,10 +27,11 @@ export interface ExtensionMessage { | "workspaceUpdated" | "invoke" | "partialMessage" - | "glamaModels" | "openRouterModels" - | "openAiModels" + | "glamaModels" + | "unboundModels" | "requestyModels" + | "openAiModels" | "mcpServers" | "enhancedPrompt" | "commitSearchResults" @@ -43,8 +44,6 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" - | "unboundModels" - | "refreshUnboundModels" | "currentCheckpointUpdated" text?: string action?: @@ -67,11 +66,11 @@ export interface ExtensionMessage { path?: string }> partialMessage?: ClineMessage - glamaModels?: Record - requestyModels?: Record openRouterModels?: Record - openAiModels?: string[] + glamaModels?: Record unboundModels?: Record + requestyModels?: Record + openAiModels?: string[] mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 106e6d243b..fde7442cc1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -40,11 +40,11 @@ export interface WebviewMessage { | "openFile" | "openMention" | "cancelTask" - | "refreshGlamaModels" | "refreshOpenRouterModels" - | "refreshOpenAiModels" + | "refreshGlamaModels" | "refreshUnboundModels" | "refreshRequestyModels" + | "refreshOpenAiModels" | "alwaysAllowBrowser" | "alwaysAllowMcp" | "alwaysAllowModeSwitch" @@ -71,7 +71,6 @@ export interface WebviewMessage { | "mcpEnabled" | "enableMcpServerCreation" | "searchCommits" - | "refreshGlamaModels" | "alwaysApproveResubmit" | "requestDelaySeconds" | "rateLimitSeconds" diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 1d64f934dc..22564d01a6 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -3674,6 +3674,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", @@ -4719,6 +4720,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx index 4b419957b6..06764a1bfa 100644 --- a/webview-ui/src/components/settings/ApiErrorMessage.tsx +++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx @@ -4,13 +4,13 @@ interface ApiErrorMessageProps { errorMessage: string | undefined children?: React.ReactNode } -const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => { - return ( -
- - {errorMessage} - {children} + +export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => ( +
+
+
+
{errorMessage}
- ) -} -export default ApiErrorMessage + {children} +
+) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 107f2a483a..c30035cef0 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import * as vscodemodels from "vscode" -import { Slider } from "@/components/ui" - import { ApiConfiguration, ModelInfo, @@ -33,7 +31,6 @@ import { unboundDefaultModelInfo, requestyDefaultModelId, requestyDefaultModelInfo, - THINKING_BUDGET, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" @@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles" import { ModelPicker } from "./ModelPicker" import { TemperatureControl } from "./TemperatureControl" import { validateApiConfiguration, validateModelId } from "@/utils/validate" -import ApiErrorMessage from "./ApiErrorMessage" +import { ApiErrorMessage } from "./ApiErrorMessage" +import { ThinkingBudget } from "./ThinkingBudget" + +const modelsByProvider: Record> = { + anthropic: anthropicModels, + bedrock: bedrockModels, + vertex: vertexModels, + gemini: geminiModels, + "openai-native": openAiNativeModels, + deepseek: deepSeekModels, + mistral: mistralModels, +} interface ApiOptionsProps { uriScheme: string | undefined @@ -66,18 +74,23 @@ const ApiOptions = ({ const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) + const [openRouterModels, setOpenRouterModels] = useState>({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, }) + const [glamaModels, setGlamaModels] = useState>({ [glamaDefaultModelId]: glamaDefaultModelInfo, }) + const [unboundModels, setUnboundModels] = useState>({ [unboundDefaultModelId]: unboundDefaultModelInfo, }) + const [requestyModels, setRequestyModels] = useState>({ [requestyDefaultModelId]: requestyDefaultModelInfo, }) + const [openAiModels, setOpenAiModels] = useState | null>(null) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) @@ -85,8 +98,6 @@ const ApiOptions = ({ const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default - const noTransform = (value: T) => value const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any const dropdownEventTransform = (event: DropdownOption | string | undefined) => @@ -103,62 +114,87 @@ const ApiOptions = ({ [setApiConfigurationField], ) - const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) + const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo( + () => normalizeApiConfiguration(apiConfiguration), + [apiConfiguration], + ) - // Pull ollama/lmstudio models - // Debounced model updates, only executed 250ms after the user stops typing + // Debounced refresh model updates, only executed 250ms after the user + // stops typing. useDebounce( () => { - if (selectedProvider === "ollama") { + if (selectedProvider === "openrouter") { + vscode.postMessage({ type: "refreshOpenRouterModels" }) + } else if (selectedProvider === "glama") { + vscode.postMessage({ type: "refreshGlamaModels" }) + } else if (selectedProvider === "unbound") { + vscode.postMessage({ type: "refreshUnboundModels" }) + } else if (selectedProvider === "requesty") { + vscode.postMessage({ + type: "refreshRequestyModels", + values: { apiKey: apiConfiguration?.requestyApiKey }, + }) + } else if (selectedProvider === "openai") { + vscode.postMessage({ + type: "refreshOpenAiModels", + values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey }, + }) + } else if (selectedProvider === "ollama") { vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl }) } else if (selectedProvider === "lmstudio") { vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl }) } else if (selectedProvider === "vscode-lm") { vscode.postMessage({ type: "requestVsCodeLmModels" }) - } else if (selectedProvider === "openai") { - vscode.postMessage({ - type: "refreshOpenAiModels", - values: { - baseUrl: apiConfiguration?.openAiBaseUrl, - apiKey: apiConfiguration?.openAiApiKey, - }, - }) - } else if (selectedProvider === "openrouter") { - vscode.postMessage({ type: "refreshOpenRouterModels", values: {} }) - } else if (selectedProvider === "glama") { - vscode.postMessage({ type: "refreshGlamaModels", values: {} }) - } else if (selectedProvider === "requesty") { - vscode.postMessage({ - type: "refreshRequestyModels", - values: { - apiKey: apiConfiguration?.requestyApiKey, - }, - }) } }, 250, [ selectedProvider, - apiConfiguration?.ollamaBaseUrl, - apiConfiguration?.lmStudioBaseUrl, + apiConfiguration?.requestyApiKey, apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, - apiConfiguration?.requestyApiKey, + apiConfiguration?.ollamaBaseUrl, + apiConfiguration?.lmStudioBaseUrl, ], ) useEffect(() => { const apiValidationResult = validateApiConfiguration(apiConfiguration) || - validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels) - setErrorMessage(apiValidationResult) - }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels]) + validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels) - const handleMessage = useCallback((event: MessageEvent) => { + setErrorMessage(apiValidationResult) + }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels]) + + const onMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data + switch (message.type) { + case "openRouterModels": { + const updatedModels = message.openRouterModels ?? {} + setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels }) + break + } + case "glamaModels": { + const updatedModels = message.glamaModels ?? {} + setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels }) + break + } + case "unboundModels": { + const updatedModels = message.unboundModels ?? {} + setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels }) + break + } + case "requestyModels": { + const updatedModels = message.requestyModels ?? {} + setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels }) + break + } + case "openAiModels": { + const updatedModels = message.openAiModels ?? [] + setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults]))) + break + } case "ollamaModels": { const newModels = message.ollamaModels ?? [] @@ -177,72 +213,30 @@ const ApiOptions = ({ setVsCodeLmModels(newModels) } break - case "glamaModels": { - const updatedModels = message.glamaModels ?? {} - setGlamaModels({ - [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openRouterModels": { - const updatedModels = message.openRouterModels ?? {} - setOpenRouterModels({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openAiModels": { - const updatedModels = message.openAiModels ?? [] - setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults]))) - break - } - case "unboundModels": { - const updatedModels = message.unboundModels ?? {} - setUnboundModels(updatedModels) - break - } - case "requestyModels": { - const updatedModels = message.requestyModels ?? {} - setRequestyModels({ - [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } } }, []) - useEvent("message", handleMessage) + useEvent("message", onMessage) - const createDropdown = (models: Record) => { - const options: DropdownOption[] = [ - { value: "", label: "Select a model..." }, - ...Object.keys(models).map((modelId) => ({ - value: modelId, - label: modelId, - })), - ] - - return ( - { - setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) - }} - style={{ width: "100%" }} - options={options} - /> - ) - } + const selectedProviderModelOptions: DropdownOption[] = useMemo( + () => + modelsByProvider[selectedProvider] + ? [ + { value: "", label: "Select a model..." }, + ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({ + value: modelId, + label: modelId, + })), + ] + : [], + [selectedProvider], + ) return (
-
+ {errorMessage && } + {selectedProvider === "anthropic" && (
- Anthropic API Key + Anthropic API Key - Glama API Key + Glama API Key {!apiConfiguration?.glamaApiKey && ( - Requesty API Key + Requesty API Key

- OpenAI API Key + OpenAI API Key

- Mistral API Key + Mistral API Key

- Codestral Base URL (Optional) + Codestral Base URL (Optional)

- OpenRouter API Key + OpenRouter API Key {!apiConfiguration?.openRouterApiKey && (

@@ -530,7 +526,7 @@ const ApiOptions = ({ style={{ width: "100%" }} onInput={handleInputChange("awsProfile")} placeholder="Enter profile name"> - AWS Profile Name + AWS Profile Name ) : ( <> @@ -541,7 +537,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("awsAccessKey")} placeholder="Enter Access Key..."> - AWS Access Key + AWS Access Key - AWS Secret Key + AWS Secret Key - AWS Session Token + AWS Session Token )}

- Google Cloud Project ID + Google Cloud Project ID
- {errorMessage && }

- Gemini API Key + Gemini API Key

- Base URL + Base URL - API Key + API Key

)} - -
+
- Max Output Tokens + Max Output Tokens
- Context Window Size + Context Window Size
- Image Support + Image Support - Computer Use + Computer Use
- Input Price + Input Price
- Output Price + Output Price - Base URL (optional) + Base URL (optional) - Model ID + Model ID - {errorMessage && } - {lmStudioModels.length > 0 && ( {" "} feature to use it with this extension.{" "} - (Note: Roo Code uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -1154,7 +1141,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("deepSeekApiKey")} placeholder="Enter API Key..."> - DeepSeek API Key + DeepSeek API Key

{vsCodeLmModels.length > 0 ? ( - Base URL (optional) + Base URL (optional) - Model ID + Model ID {errorMessage && (
@@ -1284,7 +1271,7 @@ const ApiOptions = ({ quickstart guide. - (Note: Roo Code uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -1299,7 +1286,7 @@ const ApiOptions = ({ type="password" onChange={handleInputChange("unboundApiKey")} placeholder="Enter API Key..."> - Unbound API Key + Unbound API Key {!apiConfiguration?.unboundApiKey && ( This key is stored locally and only used to make API requests from this extension.

-
)} - {selectedProvider === "glama" && ( - - )} - {selectedProvider === "openrouter" && ( )} + + {selectedProvider === "glama" && ( + + )} + + {selectedProvider === "unbound" && ( + + )} + {selectedProvider === "requesty" && ( )} - {selectedProvider !== "glama" && - selectedProvider !== "openrouter" && - selectedProvider !== "requesty" && - selectedProvider !== "openai" && - selectedProvider !== "ollama" && - selectedProvider !== "lmstudio" && - selectedProvider !== "unbound" && ( - <> -
- - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} - {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "vertex" && createDropdown(vertexModels)} - {selectedProvider === "gemini" && createDropdown(geminiModels)} - {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} - {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} - {selectedProvider === "mistral" && createDropdown(mistralModels)} -
- {errorMessage && } - 0 && ( + <> +
+ + { + setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) + }} + options={selectedProviderModelOptions} + className="w-full" /> - - )} - - {selectedModelInfo && selectedModelInfo.thinking && ( -
-
Thinking Budget
-
- setApiConfigurationField("anthropicThinking", value[0])} - /> -
{anthropicThinkingBudget}
-
- Number of tokens Claude is allowed to use for its internal reasoning process. -
-
+ + + )} {!fromWelcomeView && ( @@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { const getProviderData = (models: Record, defaultId: string) => { let selectedModelId: string let selectedModelInfo: ModelInfo + if (modelId && modelId in models) { selectedModelId = modelId selectedModelInfo = models[modelId] @@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId = defaultId selectedModelInfo = models[defaultId] } + return { selectedProvider: provider, selectedModelId, selectedModelInfo } } + switch (provider) { case "anthropic": return getProviderData(anthropicModels, anthropicDefaultModelId) @@ -1481,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { return getProviderData(deepSeekModels, deepSeekDefaultModelId) case "openai-native": return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) - case "glama": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId, - selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo, - } case "mistral": return getProviderData(mistralModels, mistralDefaultModelId) case "openrouter": @@ -1495,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, } + case "glama": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId, + selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo, + } + case "unbound": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, + selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, + } + case "requesty": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, + selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, + } case "openai": return { selectedProvider: provider, @@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { : "", selectedModelInfo: { ...openAiModelInfoSaneDefaults, - supportsImages: false, // VSCode LM API currently doesn't support images + supportsImages: false, // VSCode LM API currently doesn't support images. }, } - case "unbound": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, - selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, - } - case "requesty": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, - selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, - } default: return getProviderData(anthropicModels, anthropicDefaultModelId) } diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index fd62bfb97b..5a7737edd5 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,11 +1,13 @@ +import { useMemo, useState, useCallback, useEffect, useRef } from "react" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { useMemo, useState, useCallback, useEffect } from "react" + +import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox" + +import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" import { normalizeApiConfiguration } from "./ApiOptions" +import { ThinkingBudget } from "./ThinkingBudget" import { ModelInfoView } from "./ModelInfoView" -import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" -import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox" -import ApiErrorMessage from "./ApiErrorMessage" type ExtractType = NonNullable< { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration] @@ -14,24 +16,17 @@ type ExtractType = NonNullable< type ModelIdKeys = NonNullable< { [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration] > -declare module "react" { - interface CSSProperties { - // Allow CSS variables - [key: `--${string}`]: string | number - } -} + interface ModelPickerProps { - defaultModelId?: string + defaultModelId: string + defaultModelInfo?: ModelInfo models: Record | null modelIdKey: ModelIdKeys modelInfoKey: ExtractType serviceName: string serviceUrl: string - recommendedModel: string apiConfiguration: ApiConfiguration setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void - defaultModelInfo?: ModelInfo - errorMessage?: string } export const ModelPicker = ({ @@ -41,13 +36,12 @@ export const ModelPicker = ({ modelInfoKey, serviceName, serviceUrl, - recommendedModel, apiConfiguration, setApiConfigurationField, defaultModelInfo, - errorMessage, }: ModelPickerProps) => { const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + const isInitialized = useRef(false) const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models]) @@ -55,6 +49,7 @@ export const ModelPicker = ({ () => normalizeApiConfiguration(apiConfiguration), [apiConfiguration], ) + const onSelect = useCallback( (modelId: string) => { const modelInfo = models?.[modelId] @@ -63,26 +58,23 @@ export const ModelPicker = ({ }, [modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo], ) + + const inputValue = apiConfiguration[modelIdKey] + useEffect(() => { - if (apiConfiguration[modelIdKey] == null && defaultModelId) { - onSelect(defaultModelId) + if (!inputValue && !isInitialized.current) { + const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId + setApiConfigurationField(modelIdKey, initialValue) } - }, [apiConfiguration, defaultModelId, modelIdKey, onSelect]) + + isInitialized.current = true + }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) return ( <>
Model
- - + + No model found. {modelIds.map((model) => ( @@ -92,31 +84,18 @@ export const ModelPicker = ({ ))} - - {errorMessage ? ( - -

- - Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected. - -

-
- ) : ( - selectedModelId && - selectedModelInfo && ( - - ) + + {selectedModelId && selectedModelInfo && selectedModelId === inputValue && ( + )}

The extension automatically fetches the latest list of models available on{" "} @@ -124,7 +103,7 @@ export const ModelPicker = ({ {serviceName}. If you're unsure which model to choose, Roo Code works best with{" "} - onSelect(recommendedModel)}>{recommendedModel}. + onSelect(defaultModelId)}>{defaultModelId}. You can also try searching "free" for no-cost options currently available.

diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index ee032c3ee0..d3e65a99ea 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -66,21 +66,20 @@ const SettingsView = forwardRef(({ onDone }, terminalOutputLineLimit, writeDelayMs, } = cachedState - + //Make sure apiConfiguration is initialized and managed by SettingsView const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) useEffect(() => { - // Update only when currentApiConfigName is changed - // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration + // Update only when currentApiConfigName is changed. + // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration. if (prevApiConfigName.current === currentApiConfigName) { return } - setCachedState((prevCachedState) => ({ - ...prevCachedState, - ...extensionState, - })) + + setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName + // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false") setChangeDetected(false) }, [currentApiConfigName, extensionState, isChangeDetected]) @@ -90,11 +89,10 @@ const SettingsView = forwardRef(({ onDone }, if (prevState[field] === value) { return prevState } + + // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - [field]: value, - } + return { ...prevState, [field]: value } }) }, [], @@ -107,15 +105,10 @@ const SettingsView = forwardRef(({ onDone }, return prevState } + // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - apiConfiguration: { - ...prevState.apiConfiguration, - [field]: value, - }, - } + return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } }) }, [], @@ -126,14 +119,19 @@ const SettingsView = forwardRef(({ onDone }, if (prevState.experiments?.[id] === enabled) { return prevState } + + // console.log("setExperimentEnabled: setChangeDetected -> true") setChangeDetected(true) + return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled }, } }) }, []) + const isSettingValid = !errorMessage + const handleSubmit = () => { if (isSettingValid) { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) @@ -160,6 +158,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) + // console.log("handleSubmit: setChangeDetected -> false") setChangeDetected(false) } } @@ -176,13 +175,7 @@ const SettingsView = forwardRef(({ onDone }, [isChangeDetected], ) - useImperativeHandle( - ref, - () => ({ - checkUnsaveChanges, - }), - [checkUnsaveChanges], - ) + useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges]) const onConfirmDialogResult = useCallback((confirm: boolean) => { if (confirm) { @@ -200,10 +193,7 @@ const SettingsView = forwardRef(({ onDone }, const newCommands = [...currentCommands, commandInput] setCachedStateField("allowedCommands", newCommands) setCommandInput("") - vscode.postMessage({ - type: "allowedCommands", - commands: newCommands, - }) + vscode.postMessage({ type: "allowedCommands", commands: newCommands }) } } diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx new file mode 100644 index 0000000000..efaa90dc39 --- /dev/null +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -0,0 +1,29 @@ +import { Slider } from "@/components/ui" + +import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api" + +interface ThinkingBudgetProps { + apiConfiguration: ApiConfiguration + setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void + modelInfo?: ModelInfo +} + +export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { + const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default + + return modelInfo && modelInfo.thinking ? ( +
+
Thinking Budget
+
+ setApiConfigurationField("anthropicThinking", value[0])} + /> +
{budget}
+
+
+ ) : null +} diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx index 7530cae54d..82a25bf8f7 100644 --- a/webview-ui/src/components/ui/alert-dialog.tsx +++ b/webview-ui/src/components/ui/alert-dialog.tsx @@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root +function AlertDialog({ ...props }: React.ComponentProps) { + return +} -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +function AlertDialogTrigger({ ...props }: React.ComponentProps) { + return +} -const AlertDialogPortal = AlertDialogPrimitive.Portal +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return +} -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - ) { + return ( + - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + ) +} -const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +function AlertDialogContent({ className, ...props }: React.ComponentProps) { + return ( + + + + + ) +} -const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName +function AlertDialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +function AlertDialogAction({ className, ...props }: React.ComponentProps) { + return +} + +function AlertDialogCancel({ className, ...props }: React.ComponentProps) { + return +} export { AlertDialog, diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx index 11d5e2d3b0..ed3160f692 100644 --- a/webview-ui/src/components/ui/dialog.tsx +++ b/webview-ui/src/components/ui/dialog.tsx @@ -1,96 +1,108 @@ -"use client" - import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Cross2Icon } from "@radix-ui/react-icons" +import { XIcon } from "lucide-react" import { cn } from "@/lib/utils" -const Dialog = DialogPrimitive.Root +function Dialog({ ...props }: React.ComponentProps) { + return +} -const DialogTrigger = DialogPrimitive.Trigger +function DialogTrigger({ ...props }: React.ComponentProps) { + return +} -const DialogPortal = DialogPrimitive.Portal +function DialogPortal({ ...props }: React.ComponentProps) { + return +} -const DialogClose = DialogPrimitive.Close +function DialogClose({ ...props }: React.ComponentProps) { + return +} -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - ) { + return ( + - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName + {...props} + /> + ) +} -const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +function DialogContent({ className, children, ...props }: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} -const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return ( + + ) +} export { Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, DialogClose, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, } diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 5d880efc0b..ae674c895f 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -12,16 +12,14 @@ const WelcomeView = () => { const handleSubmit = useCallback(() => { const error = validateApiConfiguration(apiConfiguration) + if (error) { setErrorMessage(error) return } + setErrorMessage(undefined) - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentApiConfigName, - apiConfiguration, - }) + vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) }, [apiConfiguration, currentApiConfigName]) return ( diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 97c702637c..82af23ab49 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,74 +1,83 @@ -import { ApiConfiguration } from "../../../src/shared/api" -import { ModelInfo } from "../../../src/shared/api" +import { ApiConfiguration, ModelInfo } from "../../../src/shared/api" + export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "anthropic": - if (!apiConfiguration.apiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "glama": - if (!apiConfiguration.glamaApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "bedrock": - if (!apiConfiguration.awsRegion) { - return "You must choose a region to use with AWS Bedrock." - } - break - case "openrouter": - if (!apiConfiguration.openRouterApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "vertex": - if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { - return "You must provide a valid Google Cloud Project ID and Region." - } - break - case "gemini": - if (!apiConfiguration.geminiApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai-native": - if (!apiConfiguration.openAiNativeApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "mistral": - if (!apiConfiguration.mistralApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai": - if ( - !apiConfiguration.openAiBaseUrl || - !apiConfiguration.openAiApiKey || - !apiConfiguration.openAiModelId - ) { - return "You must provide a valid base URL, API key, and model ID." - } - break - case "ollama": - if (!apiConfiguration.ollamaModelId) { - return "You must provide a valid model ID." - } - break - case "lmstudio": - if (!apiConfiguration.lmStudioModelId) { - return "You must provide a valid model ID." - } - break - case "vscode-lm": - if (!apiConfiguration.vsCodeLmModelSelector) { - return "You must provide a valid model selector." - } - break - } + if (!apiConfiguration) { + return undefined } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + if (!apiConfiguration.openRouterApiKey) { + return "You must provide a valid API key." + } + break + case "glama": + if (!apiConfiguration.glamaApiKey) { + return "You must provide a valid API key." + } + break + case "unbound": + if (!apiConfiguration.unboundApiKey) { + return "You must provide a valid API key." + } + break + case "requesty": + if (!apiConfiguration.requestyApiKey) { + return "You must provide a valid API key." + } + break + case "anthropic": + if (!apiConfiguration.apiKey) { + return "You must provide a valid API key." + } + break + case "bedrock": + if (!apiConfiguration.awsRegion) { + return "You must choose a region to use with AWS Bedrock." + } + break + case "vertex": + if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { + return "You must provide a valid Google Cloud Project ID and Region." + } + break + case "gemini": + if (!apiConfiguration.geminiApiKey) { + return "You must provide a valid API key." + } + break + case "openai-native": + if (!apiConfiguration.openAiNativeApiKey) { + return "You must provide a valid API key." + } + break + case "mistral": + if (!apiConfiguration.mistralApiKey) { + return "You must provide a valid API key." + } + break + case "openai": + if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { + return "You must provide a valid base URL, API key, and model ID." + } + break + case "ollama": + if (!apiConfiguration.ollamaModelId) { + return "You must provide a valid model ID." + } + break + case "lmstudio": + if (!apiConfiguration.lmStudioModelId) { + return "You must provide a valid model ID." + } + break + case "vscode-lm": + if (!apiConfiguration.vsCodeLmModelSelector) { + return "You must provide a valid model selector." + } + break + } + return undefined } @@ -77,40 +86,81 @@ export function validateModelId( glamaModels?: Record, openRouterModels?: Record, unboundModels?: Record, + requestyModels?: Record, ): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "glama": - const glamaModelId = apiConfiguration.glamaModelId - if (!glamaModelId) { - return "You must provide a model ID." - } - if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "openrouter": - const modelId = apiConfiguration.openRouterModelId - if (!modelId) { - return "You must provide a model ID." - } - if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "unbound": - const unboundModelId = apiConfiguration.unboundModelId - if (!unboundModelId) { - return "You must provide a model ID." - } - if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - } + if (!apiConfiguration) { + return undefined } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + const modelId = apiConfiguration.openRouterModelId + + if (!modelId) { + return "You must provide a model ID." + } + + if ( + openRouterModels && + Object.keys(openRouterModels).length > 1 && + !Object.keys(openRouterModels).includes(modelId) + ) { + return `The model ID (${modelId}) you provided is not available. Please choose a different model.` + } + + break + + case "glama": + const glamaModelId = apiConfiguration.glamaModelId + + if (!glamaModelId) { + return "You must provide a model ID." + } + + if ( + glamaModels && + Object.keys(glamaModels).length > 1 && + !Object.keys(glamaModels).includes(glamaModelId) + ) { + return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "unbound": + const unboundModelId = apiConfiguration.unboundModelId + + if (!unboundModelId) { + return "You must provide a model ID." + } + + if ( + unboundModels && + Object.keys(unboundModels).length > 1 && + !Object.keys(unboundModels).includes(unboundModelId) + ) { + return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "requesty": + const requestyModelId = apiConfiguration.requestyModelId + + if (!requestyModelId) { + return "You must provide a model ID." + } + + if ( + requestyModels && + Object.keys(requestyModels).length > 1 && + !Object.keys(requestyModels).includes(requestyModelId) + ) { + return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.` + } + + break + } + return undefined } From 44724e5881ab8daefa7b80bb6c52e5f1bb8a828e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Feb 2025 07:06:25 +0000 Subject: [PATCH 30/32] changeset version bump --- .changeset/cold-poems-change.md | 5 ----- .changeset/real-ties-destroy.md | 5 ----- .changeset/shaggy-spies-kneel.md | 5 ----- .changeset/swift-kings-attack.md | 5 ----- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 7 files changed, 12 insertions(+), 23 deletions(-) delete mode 100644 .changeset/cold-poems-change.md delete mode 100644 .changeset/real-ties-destroy.md delete mode 100644 .changeset/shaggy-spies-kneel.md delete mode 100644 .changeset/swift-kings-attack.md diff --git a/.changeset/cold-poems-change.md b/.changeset/cold-poems-change.md deleted file mode 100644 index 41693ccdfc..0000000000 --- a/.changeset/cold-poems-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -v3.7.5 diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md deleted file mode 100644 index a2e9ba8eb0..0000000000 --- a/.changeset/real-ties-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix model picker diff --git a/.changeset/shaggy-spies-kneel.md b/.changeset/shaggy-spies-kneel.md deleted file mode 100644 index d137cf85ef..0000000000 --- a/.changeset/shaggy-spies-kneel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Add drag-and-drop for files diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md deleted file mode 100644 index 8a8a425611..0000000000 --- a/.changeset/swift-kings-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Pass "thinking" params to OpenRouter diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fb754097..7a9b4b57bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Roo Code Changelog +## 3.7.5 + +### Patch Changes + +- v3.7.5 +- Fix model picker +- Add drag-and-drop for files +- Pass "thinking" params to OpenRouter + ## [3.7.4] - Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. diff --git a/package-lock.json b/package-lock.json index 4bcdf8136d..a6c75bd69b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.7.4", + "version": "3.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.7.4", + "version": "3.7.5", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", diff --git a/package.json b/package.json index 28045436e6..40bb6a545d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A whole dev team of AI agents in your editor.", "publisher": "RooVeterinaryInc", - "version": "3.7.4", + "version": "3.7.5", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 319f16eb0aabb8033ef94d006b983bdbb45a1eec Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 23:11:42 -0800 Subject: [PATCH 31/32] Update CHANGELOG --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9b4b57bf..ee107be67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,11 @@ # Roo Code Changelog -## 3.7.5 +## [3.7.5] -### Patch Changes - -- v3.7.5 -- Fix model picker +- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173)) +- Fix various issues with the model picker - Add drag-and-drop for files -- Pass "thinking" params to OpenRouter +- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter ## [3.7.4] From da1b31765ed05cab6c1d0e25cbae6748aa7ce89a Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 23:13:52 -0800 Subject: [PATCH 32/32] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee107be67a..02a4a30cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## [3.7.5] - Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173)) -- Fix various issues with the model picker +- Fix various issues with the model picker (thanks @System233!) +- Fix model input / output cost parsing (thanks @System233!) - Add drag-and-drop for files - Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter