From c6092ce170492ae99bbb3fffe39706212474f17f Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 25 Feb 2025 01:03:12 -0800 Subject: [PATCH 01/38] 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 02/38] 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 03/38] 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 04/38] 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 05/38] 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 17/38] 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 18/38] 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 19/38] 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 20/38] 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 21/38] 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 22/38] 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 23/38] 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 24/38] 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 25/38] 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 26/38] 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 27/38] 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 From 78d5af491aed08505afcfb80bd02a96cb0d287ae Mon Sep 17 00:00:00 2001 From: Joe Manley Date: Wed, 26 Feb 2025 09:32:13 -0800 Subject: [PATCH 28/38] Fix long strings correctly in ChatRow --- webview-ui/src/components/chat/ChatRow.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index b139c68f96..4017ccf318 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -617,8 +617,10 @@ export const ChatRowContent = ({ color: "var(--vscode-badge-foreground)", borderRadius: "3px", padding: "9px", - whiteSpace: "pre-line", - wordWrap: "break-word", + overflow: "hidden", + whiteSpace: "pre-wrap", + wordBreak: "break-word", + overflowWrap: "anywhere", }}>
Date: Wed, 26 Feb 2025 09:41:57 -0800 Subject: [PATCH 29/38] Add changeset --- .changeset/fluffy-apples-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fluffy-apples-attack.md diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md new file mode 100644 index 0000000000..924a1b2505 --- /dev/null +++ b/.changeset/fluffy-apples-attack.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Handle really long text in the ChatRow similar to TaskHeader From d7266be3feb018c5bf207135cb54f74c94d198e5 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 26 Feb 2025 13:52:38 -0500 Subject: [PATCH 30/38] Better OpenRouter error handling --- .changeset/tender-cycles-help.md | 5 +++++ src/core/Cline.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-cycles-help.md diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md new file mode 100644 index 0000000000..d43e423ee6 --- /dev/null +++ b/.changeset/tender-cycles-help.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Better OpenRouter error handling diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 073bd10911..2e29ad453c 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1010,7 +1010,7 @@ export class Cline { } catch (error) { // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. if (alwaysApproveResubmit) { - const errorMsg = error.message ?? "Unknown error" + const errorMsg = error.error?.metadata?.raw ?? error.message ?? "Unknown error" const baseDelay = requestDelaySeconds || 5 const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) // Wait for the greater of the exponential delay or the rate limit delay From a4e58700ce7527ed486a96df6cce9709fb145074 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 26 Feb 2025 16:04:32 -0500 Subject: [PATCH 31/38] Support multiple files in drag-and-drop --- .changeset/orange-zoos-train.md | 5 + .../src/components/chat/ChatTextArea.tsx | 37 ++- .../chat/__tests__/ChatTextArea.test.tsx | 238 ++++++++++++++++++ 3 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 .changeset/orange-zoos-train.md diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md new file mode 100644 index 0000000000..76c16f4567 --- /dev/null +++ b/.changeset/orange-zoos-train.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Support multiple files in drag-and-drop diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index dc78a3fdb3..be2b2a9798 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -590,15 +590,36 @@ const ChatTextArea = forwardRef( const files = Array.from(e.dataTransfer.files) const text = e.dataTransfer.getData("text") if (text) { - // Convert the path to a mention-friendly format - const mentionText = convertToMentionPath(text, cwd) + // Split text on newlines to handle multiple files + const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "") - const newValue = - inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition) - setInputValue(newValue) - const newCursorPosition = cursorPosition + mentionText.length + 1 - setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + if (lines.length > 0) { + // Process each line as a separate file path + let newValue = inputValue.slice(0, cursorPosition) + let totalLength = 0 + + lines.forEach((line, index) => { + // Convert each path to a mention-friendly format + const mentionText = convertToMentionPath(line, cwd) + newValue += mentionText + totalLength += mentionText.length + + // Add space after each mention except the last one + if (index < lines.length - 1) { + newValue += " " + totalLength += 1 + } + }) + + // Add space after the last mention and append the rest of the input + newValue += " " + inputValue.slice(cursorPosition) + totalLength += 1 + + setInputValue(newValue) + const newCursorPosition = cursorPosition + totalLength + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + } return } diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx index 205912fc15..3241010e88 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx @@ -3,6 +3,7 @@ import ChatTextArea from "../ChatTextArea" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" import { defaultModeSlug } from "../../../../../src/shared/modes" +import * as pathMentions from "../../../utils/path-mentions" // Mock modules jest.mock("../../../utils/vscode", () => ({ @@ -12,9 +13,20 @@ jest.mock("../../../utils/vscode", () => ({ })) jest.mock("../../../components/common/CodeBlock") jest.mock("../../../components/common/MarkdownBlock") +jest.mock("../../../utils/path-mentions", () => ({ + convertToMentionPath: jest.fn((path, cwd) => { + // Simple mock implementation that mimics the real function's behavior + if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) { + const relativePath = path.substring(cwd.length) + return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath) + } + return path + }), +})) // Get the mocked postMessage function const mockPostMessage = vscode.postMessage as jest.Mock +const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock // Mock ExtensionStateContext jest.mock("../../../context/ExtensionStateContext") @@ -160,4 +172,230 @@ describe("ChatTextArea", () => { expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt") }) }) + + describe("multi-file drag and drop", () => { + const mockCwd = "/Users/test/project" + + beforeEach(() => { + jest.clearAllMocks() + ;(useExtensionState as jest.Mock).mockReturnValue({ + filePaths: [], + openedTabs: [], + cwd: mockCwd, + }) + mockConvertToMentionPath.mockClear() + }) + + it("should process multiple file paths separated by newlines", () => { + const setInputValue = jest.fn() + + const { container } = render( + , + ) + + // Create a mock dataTransfer object with text data containing multiple file paths + const dataTransfer = { + getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was called for each file path + expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2) + expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd) + expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd) + + // Verify setInputValue was called with the correct value + // The mock implementation of convertToMentionPath will convert the paths to @/file1.js and @/file2.js + expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text") + }) + + it("should filter out empty lines in the dragged text", () => { + const setInputValue = jest.fn() + + const { container } = render( + , + ) + + // Create a mock dataTransfer object with text data containing empty lines + const dataTransfer = { + getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n\n/Users/test/project/file2.js\n\n"), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was called only for non-empty lines + expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2) + + // Verify setInputValue was called with the correct value + expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text") + }) + + it("should correctly update cursor position after adding multiple mentions", () => { + const setInputValue = jest.fn() + const initialCursorPosition = 5 + + const { container } = render( + , + ) + + // Set the cursor position manually + const textArea = container.querySelector("textarea") + if (textArea) { + textArea.selectionStart = initialCursorPosition + textArea.selectionEnd = initialCursorPosition + } + + // Create a mock dataTransfer object with text data + const dataTransfer = { + getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // The cursor position should be updated based on the implementation in the component + expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Hello world") + }) + + it("should handle very long file paths correctly", () => { + const setInputValue = jest.fn() + + const { container } = render() + + // Create a very long file path + const longPath = + "/Users/test/project/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript" + + // Create a mock dataTransfer object with the long path + const dataTransfer = { + getData: jest.fn().mockReturnValue(longPath), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was called with the long path + expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd) + + // The mock implementation will convert it to @/very/long/path/... + expect(setInputValue).toHaveBeenCalledWith( + "@/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript ", + ) + }) + + it("should handle paths with special characters correctly", () => { + const setInputValue = jest.fn() + + const { container } = render() + + // Create paths with special characters + const specialPath1 = "/Users/test/project/file with spaces.js" + const specialPath2 = "/Users/test/project/file-with-dashes.js" + const specialPath3 = "/Users/test/project/file_with_underscores.js" + const specialPath4 = "/Users/test/project/file.with.dots.js" + + // Create a mock dataTransfer object with the special paths + const dataTransfer = { + getData: jest + .fn() + .mockReturnValue(`${specialPath1}\n${specialPath2}\n${specialPath3}\n${specialPath4}`), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was called for each path + expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4) + expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd) + expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd) + expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd) + expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd) + + // Verify setInputValue was called with the correct value + expect(setInputValue).toHaveBeenCalledWith( + "@/file with spaces.js @/file-with-dashes.js @/file_with_underscores.js @/file.with.dots.js ", + ) + }) + + it("should handle paths outside the current working directory", () => { + const setInputValue = jest.fn() + + const { container } = render() + + // Create paths outside the current working directory + const outsidePath = "/Users/other/project/file.js" + + // Mock the convertToMentionPath function to return the original path for paths outside cwd + mockConvertToMentionPath.mockImplementationOnce((path, cwd) => { + return path // Return original path for this test + }) + + // Create a mock dataTransfer object with the outside path + const dataTransfer = { + getData: jest.fn().mockReturnValue(outsidePath), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was called with the outside path + expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd) + + // Verify setInputValue was called with the original path + expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ") + }) + + it("should do nothing when dropped text is empty", () => { + const setInputValue = jest.fn() + + const { container } = render( + , + ) + + // Create a mock dataTransfer object with empty text + const dataTransfer = { + getData: jest.fn().mockReturnValue(""), + files: [], + } + + // Simulate drop event + fireEvent.drop(container.querySelector(".chat-text-area")!, { + dataTransfer, + preventDefault: jest.fn(), + }) + + // Verify convertToMentionPath was not called + expect(mockConvertToMentionPath).not.toHaveBeenCalled() + + // Verify setInputValue was not called + expect(setInputValue).not.toHaveBeenCalled() + }) + }) }) From 5e53d00ebcf0d2adf218a07452d0e15835bf3e64 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Wed, 26 Feb 2025 14:23:11 -0800 Subject: [PATCH 32/38] Allow control over maxTokens for thinking models --- src/api/providers/anthropic.ts | 12 ++- src/api/providers/openrouter.ts | 13 ++- src/core/Cline.ts | 18 +++- .../__tests__/sliding-window.test.ts | 102 +++++++++++++++--- src/core/sliding-window/index.ts | 37 ++++--- src/core/webview/ClineProvider.ts | 5 + src/shared/api.ts | 11 +- src/shared/globalState.ts | 1 + .../components/settings/ThinkingBudget.tsx | 63 ++++++++--- 9 files changed, 197 insertions(+), 65 deletions(-) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index ad58a1cf6b..8c5a1795b1 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -31,7 +31,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { let stream: AnthropicStream const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { id: modelId, info: modelInfo } = this.getModel() - const maxTokens = modelInfo.maxTokens || 8192 + const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens || 8192 let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE let thinking: BetaThinkingConfigParam | undefined = undefined @@ -41,7 +41,15 @@ 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) + + // Clamp the thinking budget to be at most 80% of max tokens and at + // least 1024 tokens. + const maxBudgetTokens = Math.floor(maxTokens * 0.8) + const budgetTokens = Math.max( + Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens), + 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 0a9488e816..69bcb0074c 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -108,12 +108,19 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { topP = 0.95 } + const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens let temperature = this.options.modelTemperature ?? defaultTemperature let thinking: BetaThinkingConfigParam | undefined = undefined if (modelInfo.thinking) { - const maxTokens = modelInfo.maxTokens || 8192 - const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024) + // Clamp the thinking budget to be at most 80% of max tokens and at + // least 1024 tokens. + const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8) + const budgetTokens = Math.max( + Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens), + 1024, + ) + thinking = { type: "enabled", budget_tokens: budgetTokens } temperature = 1.0 } @@ -271,7 +278,7 @@ export async function getOpenRouterModels() { modelInfo.supportsPromptCache = true modelInfo.cacheWritesPrice = 3.75 modelInfo.cacheReadsPrice = 0.3 - modelInfo.maxTokens = 16384 + modelInfo.maxTokens = 64_000 break case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"): modelInfo.supportsPromptCache = true diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 073bd10911..fb123e0584 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -87,6 +87,7 @@ export type ClineOptions = { export class Cline { readonly taskId: string + readonly apiConfiguration: ApiConfiguration api: ApiHandler private terminalManager: TerminalManager private urlContentFetcher: UrlContentFetcher @@ -148,6 +149,7 @@ export class Cline { } this.taskId = crypto.randomUUID() + this.apiConfiguration = apiConfiguration this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() this.urlContentFetcher = new UrlContentFetcher(provider.context) @@ -961,13 +963,21 @@ export class Cline { cacheWrites = 0, cacheReads = 0, }: ClineApiReqInfo = JSON.parse(previousRequest) + const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads - const trimmedMessages = truncateConversationIfNeeded( - this.apiConversationHistory, + const modelInfo = this.api.getModel().info + const maxTokens = modelInfo.thinking + ? this.apiConfiguration.modelMaxTokens || modelInfo.maxTokens + : modelInfo.maxTokens + const contextWindow = modelInfo.contextWindow + + const trimmedMessages = truncateConversationIfNeeded({ + messages: this.apiConversationHistory, totalTokens, - this.api.getModel().info, - ) + maxTokens, + contextWindow, + }) if (trimmedMessages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(trimmedMessages) diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts index 3dcf9e5fd2..cb897aa8cb 100644 --- a/src/core/sliding-window/__tests__/sliding-window.test.ts +++ b/src/core/sliding-window/__tests__/sliding-window.test.ts @@ -119,11 +119,21 @@ describe("getMaxTokens", () => { // Max tokens = 100000 - 50000 = 50000 // Below max tokens - no truncation - const result1 = truncateConversationIfNeeded(messages, 49999, modelInfo) + const result1 = truncateConversationIfNeeded({ + messages, + totalTokens: 49999, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result1).toEqual(messages) // Above max tokens - truncate - const result2 = truncateConversationIfNeeded(messages, 50001, modelInfo) + const result2 = truncateConversationIfNeeded({ + messages, + totalTokens: 50001, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result2).not.toEqual(messages) expect(result2.length).toBe(3) // Truncated with 0.5 fraction }) @@ -133,11 +143,21 @@ describe("getMaxTokens", () => { // Max tokens = 100000 - (100000 * 0.2) = 80000 // Below max tokens - no truncation - const result1 = truncateConversationIfNeeded(messages, 79999, modelInfo) + const result1 = truncateConversationIfNeeded({ + messages, + totalTokens: 79999, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result1).toEqual(messages) // Above max tokens - truncate - const result2 = truncateConversationIfNeeded(messages, 80001, modelInfo) + const result2 = truncateConversationIfNeeded({ + messages, + totalTokens: 80001, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result2).not.toEqual(messages) expect(result2.length).toBe(3) // Truncated with 0.5 fraction }) @@ -147,11 +167,21 @@ describe("getMaxTokens", () => { // Max tokens = 50000 - 10000 = 40000 // Below max tokens - no truncation - const result1 = truncateConversationIfNeeded(messages, 39999, modelInfo) + const result1 = truncateConversationIfNeeded({ + messages, + totalTokens: 39999, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result1).toEqual(messages) // Above max tokens - truncate - const result2 = truncateConversationIfNeeded(messages, 40001, modelInfo) + const result2 = truncateConversationIfNeeded({ + messages, + totalTokens: 40001, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result2).not.toEqual(messages) expect(result2.length).toBe(3) // Truncated with 0.5 fraction }) @@ -161,11 +191,21 @@ describe("getMaxTokens", () => { // Max tokens = 200000 - 30000 = 170000 // Below max tokens - no truncation - const result1 = truncateConversationIfNeeded(messages, 169999, modelInfo) + const result1 = truncateConversationIfNeeded({ + messages, + totalTokens: 169999, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result1).toEqual(messages) // Above max tokens - truncate - const result2 = truncateConversationIfNeeded(messages, 170001, modelInfo) + const result2 = truncateConversationIfNeeded({ + messages, + totalTokens: 170001, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result2).not.toEqual(messages) expect(result2.length).toBe(3) // Truncated with 0.5 fraction }) @@ -194,7 +234,12 @@ describe("truncateConversationIfNeeded", () => { const maxTokens = 100000 - 30000 // 70000 const totalTokens = 69999 // Below threshold - const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo) + const result = truncateConversationIfNeeded({ + messages, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result).toEqual(messages) // No truncation occurs }) @@ -207,7 +252,12 @@ describe("truncateConversationIfNeeded", () => { // With 4 messages after the first, 0.5 fraction means remove 2 messages const expectedResult = [messages[0], messages[3], messages[4]] - const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo) + const result = truncateConversationIfNeeded({ + messages, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + }) expect(result).toEqual(expectedResult) }) @@ -218,14 +268,38 @@ describe("truncateConversationIfNeeded", () => { // Test below threshold const belowThreshold = 69999 - expect(truncateConversationIfNeeded(messages, belowThreshold, modelInfo1)).toEqual( - truncateConversationIfNeeded(messages, belowThreshold, modelInfo2), + expect( + truncateConversationIfNeeded({ + messages, + totalTokens: belowThreshold, + contextWindow: modelInfo1.contextWindow, + maxTokens: modelInfo1.maxTokens, + }), + ).toEqual( + truncateConversationIfNeeded({ + messages, + totalTokens: belowThreshold, + contextWindow: modelInfo2.contextWindow, + maxTokens: modelInfo2.maxTokens, + }), ) // Test above threshold const aboveThreshold = 70001 - expect(truncateConversationIfNeeded(messages, aboveThreshold, modelInfo1)).toEqual( - truncateConversationIfNeeded(messages, aboveThreshold, modelInfo2), + expect( + truncateConversationIfNeeded({ + messages, + totalTokens: aboveThreshold, + contextWindow: modelInfo1.contextWindow, + maxTokens: modelInfo1.maxTokens, + }), + ).toEqual( + truncateConversationIfNeeded({ + messages, + totalTokens: aboveThreshold, + contextWindow: modelInfo2.contextWindow, + maxTokens: modelInfo2.maxTokens, + }), ) }) }) diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index a0fff05ea5..8b646f933b 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -1,7 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { ModelInfo } from "../../shared/api" - /** * Truncates a conversation by removing a fraction of the messages. * @@ -26,28 +24,29 @@ export function truncateConversation( } /** - * Conditionally truncates the conversation messages if the total token count exceeds the model's limit. + * Conditionally truncates the conversation messages if the total token count + * exceeds the model's limit. * * @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages. * @param {number} totalTokens - The total number of tokens in the conversation. - * @param {ModelInfo} modelInfo - Model metadata including context window size. + * @param {number} contextWindow - The context window size. + * @param {number} maxTokens - The maximum number of tokens allowed. * @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages. */ -export function truncateConversationIfNeeded( - messages: Anthropic.Messages.MessageParam[], - totalTokens: number, - modelInfo: ModelInfo, -): Anthropic.Messages.MessageParam[] { - return totalTokens < getMaxTokens(modelInfo) ? messages : truncateConversation(messages, 0.5) + +type TruncateOptions = { + messages: Anthropic.Messages.MessageParam[] + totalTokens: number + contextWindow: number + maxTokens?: number } -/** - * Calculates the maximum allowed tokens - * - * @param {ModelInfo} modelInfo - The model information containing the context window size. - * @returns {number} The maximum number of tokens allowed - */ -function getMaxTokens(modelInfo: ModelInfo): number { - // The buffer needs to be at least as large as `modelInfo.maxTokens`, or 20% of the context window if for some reason it's not set. - return modelInfo.contextWindow - (modelInfo.maxTokens || modelInfo.contextWindow * 0.2) +export function truncateConversationIfNeeded({ + messages, + totalTokens, + contextWindow, + maxTokens, +}: TruncateOptions): Anthropic.Messages.MessageParam[] { + const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2) + return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bc6f457868..5e6170e2ee 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1671,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyModelId, requestyModelInfo, modelTemperature, + modelMaxTokens, } = apiConfiguration await Promise.all([ this.updateGlobalState("apiProvider", apiProvider), @@ -1719,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.updateGlobalState("requestyModelId", requestyModelId), this.updateGlobalState("requestyModelInfo", requestyModelInfo), this.updateGlobalState("modelTemperature", modelTemperature), + this.updateGlobalState("modelMaxTokens", modelMaxTokens), ]) if (this.cline) { this.cline.api = buildApiHandler(apiConfiguration) @@ -2210,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyModelId, requestyModelInfo, modelTemperature, + modelMaxTokens, maxOpenTabsContext, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, @@ -2293,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("requestyModelId") as Promise, this.getGlobalState("requestyModelInfo") as Promise, this.getGlobalState("modelTemperature") as Promise, + this.getGlobalState("modelMaxTokens") as Promise, this.getGlobalState("maxOpenTabsContext") as Promise, ]) @@ -2358,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestyModelId, requestyModelInfo, modelTemperature, + modelMaxTokens, }, lastShownAnnouncementId, customInstructions, diff --git a/src/shared/api.ts b/src/shared/api.ts index 5d4b8b120d..e7e4c54db6 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -68,6 +68,7 @@ export interface ApiHandlerOptions { requestyModelId?: string requestyModelInfo?: ModelInfo modelTemperature?: number + modelMaxTokens?: number } export type ApiConfiguration = ApiHandlerOptions & { @@ -92,19 +93,13 @@ export interface ModelInfo { thinking?: boolean } -export const THINKING_BUDGET = { - step: 1024, - min: 1024, - default: 8 * 1024, -} - // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219" export const anthropicModels = { "claude-3-7-sonnet-20250219:thinking": { - maxTokens: 16384, + maxTokens: 64_000, contextWindow: 200_000, supportsImages: true, supportsComputerUse: true, @@ -116,7 +111,7 @@ export const anthropicModels = { thinking: true, }, "claude-3-7-sonnet-20250219": { - maxTokens: 16384, + maxTokens: 64_000, contextWindow: 200_000, supportsImages: true, supportsComputerUse: true, diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 7b6b4f8274..2cc90456a7 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -81,5 +81,6 @@ export type GlobalStateKey = | "requestyModelInfo" | "unboundModelInfo" | "modelTemperature" + | "modelMaxTokens" | "mistralCodestralUrl" | "maxOpenTabsContext" diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index efaa90dc39..5b67874410 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -1,6 +1,8 @@ +import { useEffect } from "react" + import { Slider } from "@/components/ui" -import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api" +import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" interface ThinkingBudgetProps { apiConfiguration: ApiConfiguration @@ -9,21 +11,52 @@ interface ThinkingBudgetProps { } export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { - const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default + const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000 + const tokensMin = 8192 + const tokensMax = modelInfo?.maxTokens || 64_000 - return modelInfo && modelInfo.thinking ? ( -
-
Thinking Budget
-
- setApiConfigurationField("anthropicThinking", value[0])} - /> -
{budget}
+ const thinkingTokens = apiConfiguration?.anthropicThinking || 8192 + const thinkingTokensMin = 1024 + const thinkingTokensMax = Math.floor(0.8 * tokens) + + useEffect(() => { + if (thinkingTokens > thinkingTokensMax) { + setApiConfigurationField("anthropicThinking", thinkingTokensMax) + } + }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField]) + + if (!modelInfo || !modelInfo.thinking) { + return null + } + + return ( +
+
+
Max Tokens
+
+ setApiConfigurationField("modelMaxTokens", value)} + /> +
{tokens}
+
+
+
+
Max Thinking Tokens
+
+ setApiConfigurationField("anthropicThinking", value)} + /> +
{thinkingTokens}
+
- ) : null + ) } From cf69b0fff92e8b1cff23cf4b37d8e8a2f10a34dc Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Wed, 26 Feb 2025 14:26:27 -0800 Subject: [PATCH 33/38] Add changeset --- .changeset/wild-emus-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wild-emus-dream.md diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md new file mode 100644 index 0000000000..19e5a4626b --- /dev/null +++ b/.changeset/wild-emus-dream.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Allow control over maxTokens for thinking models From dfa019e7f443bf9997ed2ac2556f597a59e4bf77 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 26 Feb 2025 17:34:39 -0500 Subject: [PATCH 34/38] Truncate results from search_files to 500 chars max --- .changeset/stale-cooks-help.md | 5 ++ src/services/ripgrep/__tests__/index.test.ts | 51 ++++++++++++++++++++ src/services/ripgrep/index.ts | 30 ++++++++++-- 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 .changeset/stale-cooks-help.md create mode 100644 src/services/ripgrep/__tests__/index.test.ts diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md new file mode 100644 index 0000000000..8c9c714738 --- /dev/null +++ b/.changeset/stale-cooks-help.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Truncate search_file output to avoid crashing the extension diff --git a/src/services/ripgrep/__tests__/index.test.ts b/src/services/ripgrep/__tests__/index.test.ts new file mode 100644 index 0000000000..7c3549a827 --- /dev/null +++ b/src/services/ripgrep/__tests__/index.test.ts @@ -0,0 +1,51 @@ +// npx jest src/services/ripgrep/__tests__/index.test.ts + +import { describe, expect, it } from "@jest/globals" +import { truncateLine } from "../index" + +describe("Ripgrep line truncation", () => { + // The default MAX_LINE_LENGTH is 500 in the implementation + const MAX_LINE_LENGTH = 500 + + it("should truncate lines longer than MAX_LINE_LENGTH", () => { + const longLine = "a".repeat(600) // Line longer than MAX_LINE_LENGTH + const truncated = truncateLine(longLine) + + expect(truncated).toContain("[truncated...]") + expect(truncated.length).toBeLessThan(longLine.length) + expect(truncated.length).toEqual(MAX_LINE_LENGTH + " [truncated...]".length) + }) + + it("should not truncate lines shorter than MAX_LINE_LENGTH", () => { + const shortLine = "Short line of text" + const truncated = truncateLine(shortLine) + + expect(truncated).toEqual(shortLine) + expect(truncated).not.toContain("[truncated...]") + }) + + it("should correctly truncate a line at exactly MAX_LINE_LENGTH characters", () => { + const exactLine = "a".repeat(MAX_LINE_LENGTH) + const exactPlusOne = exactLine + "x" + + // Should not truncate when exactly MAX_LINE_LENGTH + expect(truncateLine(exactLine)).toEqual(exactLine) + + // Should truncate when exceeding MAX_LINE_LENGTH by even 1 character + expect(truncateLine(exactPlusOne)).toContain("[truncated...]") + }) + + it("should handle empty lines without errors", () => { + expect(truncateLine("")).toEqual("") + }) + + it("should allow custom maximum length", () => { + const customLength = 100 + const line = "a".repeat(customLength + 50) + + const truncated = truncateLine(line, customLength) + + expect(truncated.length).toEqual(customLength + " [truncated...]".length) + expect(truncated).toContain("[truncated...]") + }) +}) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index b48c60b5b2..770c897e52 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -58,7 +58,19 @@ interface SearchResult { afterContext: string[] } +// Constants const MAX_RESULTS = 300 +const MAX_LINE_LENGTH = 500 + +/** + * Truncates a line if it exceeds the maximum length + * @param line The line to truncate + * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH) + * @returns The truncated line, or the original line if it's shorter than maxLength + */ +export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string { + return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line +} async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { @@ -140,7 +152,8 @@ export async function regexSearchFiles( let output: string try { output = await execRipgrep(rgPath, args) - } catch { + } catch (error) { + console.error("Error executing ripgrep:", error) return "No results found" } const results: SearchResult[] = [] @@ -154,19 +167,28 @@ export async function regexSearchFiles( if (currentResult) { results.push(currentResult as SearchResult) } + + // Safety check: truncate extremely long lines to prevent excessive output + const matchText = parsed.data.lines.text + const truncatedMatch = truncateLine(matchText) + currentResult = { file: parsed.data.path.text, line: parsed.data.line_number, column: parsed.data.submatches[0].start, - match: parsed.data.lines.text, + match: truncatedMatch, beforeContext: [], afterContext: [], } } else if (parsed.type === "context" && currentResult) { + // Apply the same truncation logic to context lines + const contextText = parsed.data.lines.text + const truncatedContext = truncateLine(contextText) + if (parsed.data.line_number < currentResult.line!) { - currentResult.beforeContext!.push(parsed.data.lines.text) + currentResult.beforeContext!.push(truncatedContext) } else { - currentResult.afterContext!.push(parsed.data.lines.text) + currentResult.afterContext!.push(truncatedContext) } } } catch (error) { From 247a50a6dcbb29512d20796b29a3240dc4b84463 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Feb 2025 23:17:23 +0000 Subject: [PATCH 35/38] changeset version bump --- .changeset/fluffy-apples-attack.md | 5 ----- .changeset/orange-zoos-train.md | 5 ----- .changeset/stale-cooks-help.md | 5 ----- .changeset/tender-cycles-help.md | 5 ----- .changeset/wild-emus-dream.md | 5 ----- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 8 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 .changeset/fluffy-apples-attack.md delete mode 100644 .changeset/orange-zoos-train.md delete mode 100644 .changeset/stale-cooks-help.md delete mode 100644 .changeset/tender-cycles-help.md delete mode 100644 .changeset/wild-emus-dream.md diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md deleted file mode 100644 index 924a1b2505..0000000000 --- a/.changeset/fluffy-apples-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Handle really long text in the ChatRow similar to TaskHeader diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md deleted file mode 100644 index 76c16f4567..0000000000 --- a/.changeset/orange-zoos-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Support multiple files in drag-and-drop diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md deleted file mode 100644 index 8c9c714738..0000000000 --- a/.changeset/stale-cooks-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Truncate search_file output to avoid crashing the extension diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md deleted file mode 100644 index d43e423ee6..0000000000 --- a/.changeset/tender-cycles-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Better OpenRouter error handling diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md deleted file mode 100644 index 19e5a4626b..0000000000 --- a/.changeset/wild-emus-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Allow control over maxTokens for thinking models diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a4a30cbd..0e5223231a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Roo Code Changelog +## 3.7.6 + +### Patch Changes + +- Handle really long text in the ChatRow similar to TaskHeader +- Support multiple files in drag-and-drop +- Truncate search_file output to avoid crashing the extension +- Better OpenRouter error handling +- Allow control over maxTokens for thinking models + ## [3.7.5] - Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173)) diff --git a/package-lock.json b/package-lock.json index a6c75bd69b..808e2f2f10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.7.5", + "version": "3.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.7.5", + "version": "3.7.6", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", diff --git a/package.json b/package.json index 40bb6a545d..463e9d597a 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.5", + "version": "3.7.6", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 4f578dc8262e03a2a665abcd6784610cc092cdb2 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 26 Feb 2025 18:47:22 -0500 Subject: [PATCH 36/38] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5223231a..13b0695335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,12 @@ # Roo Code Changelog -## 3.7.6 +## [3.7.6] -### Patch Changes - -- Handle really long text in the ChatRow similar to TaskHeader +- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!) - Support multiple files in drag-and-drop - Truncate search_file output to avoid crashing the extension -- Better OpenRouter error handling -- Allow control over maxTokens for thinking models +- Better OpenRouter error handling (no more "Provider Error") +- Add slider to control max output tokens for thinking models ## [3.7.5] From 5c5bf8502094fb87397eebadda89acd3512dcf84 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 26 Feb 2025 21:36:19 -0500 Subject: [PATCH 37/38] Stop removing commas from terminal output --- .changeset/sour-parents-hug.md | 5 +++++ src/integrations/terminal/TerminalProcess.ts | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .changeset/sour-parents-hug.md diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md new file mode 100644 index 0000000000..a24286b6bb --- /dev/null +++ b/.changeset/sour-parents-hug.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Stop removing commas from terminal output diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts index 5597350db3..4e85c10575 100644 --- a/src/integrations/terminal/TerminalProcess.ts +++ b/src/integrations/terminal/TerminalProcess.ts @@ -110,9 +110,6 @@ export class TerminalProcess extends EventEmitter { data = lines.join("\n") } - // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas. - data = data.replace(/,/g, "") - // 2. Set isHot depending on the command // Set to hot to stall API requests until terminal is cool again this.isHot = true From 4806ab5420048af6526348e5b128dd4724c9fcc8 Mon Sep 17 00:00:00 2001 From: dleffel Date: Wed, 26 Feb 2025 21:34:56 -0800 Subject: [PATCH 38/38] Fix missing tooltips in several components. --- .../src/components/chat/Announcement.tsx | 1 + .../src/components/chat/ChatTextArea.tsx | 5 +++ webview-ui/src/components/chat/ChatView.tsx | 33 ++++++++++++++++++- webview-ui/src/components/chat/TaskHeader.tsx | 13 ++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index a2e96606ef..93d0c9d750 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -25,6 +25,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => { diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index be2b2a9798..dcbe085147 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -798,6 +798,7 @@ const ChatTextArea = forwardRef( { const value = e.target.value if (value === "settings-action") { @@ -915,6 +917,7 @@ const ChatTextArea = forwardRef( role="button" aria-label="enhance prompt" data-testid="enhance-prompt-button" + title="Enhance prompt with additional context" className={`input-icon-button ${ textAreaDisabled ? "disabled" : "" } codicon codicon-sparkle`} @@ -927,11 +930,13 @@ const ChatTextArea = forwardRef( className={`input-icon-button ${ shouldDisableImages ? "disabled" : "" } codicon codicon-device-camera`} + title="Add images to message" onClick={() => !shouldDisableImages && onSelectImages()} style={{ fontSize: 16.5 }} /> !textAreaDisabled && onSend()} style={{ fontSize: 15 }} /> diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 98369cf095..fcd1ba9a3b 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie onClick={() => { scrollToBottomSmooth() disableAutoScrollRef.current = false - }}> + }} + title="Scroll to bottom of chat">
@@ -1101,6 +1102,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flex: secondaryButtonText ? 1 : 2, marginRight: secondaryButtonText ? "6px" : "0", }} + title={ + primaryButtonText === "Retry" + ? "Try the operation again" + : primaryButtonText === "Save" + ? "Save the file changes" + : primaryButtonText === "Approve" + ? "Approve this action" + : primaryButtonText === "Run Command" + ? "Execute this command" + : primaryButtonText === "Start New Task" + ? "Begin a new task" + : primaryButtonText === "Resume Task" + ? "Continue the current task" + : primaryButtonText === "Proceed Anyways" + ? "Continue despite warnings" + : primaryButtonText === "Proceed While Running" + ? "Continue while command executes" + : undefined + } onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}> {primaryButtonText} @@ -1113,6 +1133,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flex: isStreaming ? 2 : 1, marginLeft: isStreaming ? 0 : "6px", }} + title={ + isStreaming + ? "Cancel the current operation" + : secondaryButtonText === "Start New Task" + ? "Begin a new task" + : secondaryButtonText === "Reject" + ? "Reject this action" + : secondaryButtonText === "Terminate" + ? "End the current task" + : undefined + } onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}> {isStreaming ? "Cancel" : secondaryButtonText} diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 341855f796..fb7db6f617 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -180,7 +180,11 @@ const TaskHeader: React.FC = ({ ${totalCost?.toFixed(4)}
)} - +
@@ -348,13 +352,18 @@ export const highlightMentions = (text?: string, withShadow = true) => { const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
- {!!item?.size && item.size > 0 && (