Merge pull request #1399 from RooVetGit/cte/fix-sidebar-section-headers

Fix settings colors for themes that use color transparency
This commit is contained in:
Chris Estreich 2025-03-05 12:31:40 -08:00 committed by GitHub
commit 0d4a15ebab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 729 additions and 1019 deletions

View file

@ -7,17 +7,13 @@ interface VSCodeButtonLinkProps {
[key: string]: any
}
const VSCodeButtonLink: React.FC<VSCodeButtonLinkProps> = ({ href, children, ...props }) => {
return (
<a
href={href}
style={{
textDecoration: "none",
color: "inherit",
}}>
<VSCodeButton {...props}>{children}</VSCodeButton>
</a>
)
}
export default VSCodeButtonLink
export const VSCodeButtonLink = ({ href, children, ...props }: VSCodeButtonLinkProps) => (
<a
href={href}
style={{
textDecoration: "none",
color: "inherit",
}}>
<VSCodeButton {...props}>{children}</VSCodeButton>
</a>
)

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,14 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useRef, useState } from "react"
import { useRemark } from "react-remark"
import { cn } from "@/lib/utils"
import { Collapsible, CollapsibleTrigger } from "@/components/ui"
import { StyledMarkdown } from "./styles"
export const ModelDescriptionMarkdown = memo(
({
markdown,
markdown = "",
key,
isExpanded,
setIsExpanded,
@ -16,75 +19,30 @@ export const ModelDescriptionMarkdown = memo(
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
}) => {
const [reactContent, setMarkdown] = useRemark()
const [showSeeMore, setShowSeeMore] = useState(false)
const [content, setContent] = useRemark()
const [isExpandable, setIsExpandable] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMarkdown(markdown || "")
}, [markdown, setMarkdown])
useEffect(() => setContent(markdown), [markdown, setContent])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
const { scrollHeight } = textRef.current
const { clientHeight } = textContainerRef.current
const isOverflowing = scrollHeight > clientHeight
setShowSeeMore(isOverflowing)
setIsExpandable(textRef.current.scrollHeight > textContainerRef.current.clientHeight)
}
}, [reactContent, setIsExpanded])
}, [content])
return (
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
overflowY: isExpanded ? "auto" : "hidden",
position: "relative",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}>
{reactContent}
<Collapsible open={isExpanded} onOpenChange={setIsExpanded} className="relative">
<div ref={textContainerRef} className={cn({ "line-clamp-3": !isExpanded })}>
<div ref={textRef}>
<StyledMarkdown key={key}>{content}</StyledMarkdown>
</div>
{!isExpanded && showSeeMore && (
<div
style={{
position: "absolute",
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
}}>
<div
style={{
width: 30,
height: "1.2em",
background:
"linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
style={{
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more
</VSCodeLink>
</div>
)}
</div>
</StyledMarkdown>
<CollapsibleTrigger asChild className={cn({ hidden: !isExpandable })}>
<VSCodeLink className="text-sm">{isExpanded ? "Less" : "More"}</VSCodeLink>
</CollapsibleTrigger>
</Collapsible>
)
},
)

View file

@ -1,32 +1,29 @@
import { useMemo } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Fragment } from "react"
import { formatPrice } from "@/utils/formatPrice"
import { cn } from "@/lib/utils"
import { ModelInfo, geminiModels } from "../../../../src/shared/api"
import { ModelDescriptionMarkdown } from "./ModelDescriptionMarkdown"
import { formatPrice } from "../../utils/formatPrice"
type ModelInfoViewProps = {
selectedModelId: string
modelInfo: ModelInfo
isDescriptionExpanded: boolean
setIsDescriptionExpanded: (isExpanded: boolean) => void
}
export const ModelInfoView = ({
selectedModelId,
modelInfo,
isDescriptionExpanded,
setIsDescriptionExpanded,
}: {
selectedModelId: string
modelInfo: ModelInfo
isDescriptionExpanded: boolean
setIsDescriptionExpanded: (isExpanded: boolean) => void
}) => {
const isGemini = Object.keys(geminiModels).includes(selectedModelId)
}: ModelInfoViewProps) => {
const isGemini = useMemo(() => Object.keys(geminiModels).includes(selectedModelId), [selectedModelId])
const infoItems = [
modelInfo.description && (
<ModelDescriptionMarkdown
key="description"
markdown={modelInfo.description}
isExpanded={isDescriptionExpanded}
setIsExpanded={setIsDescriptionExpanded}
/>
),
<ModelInfoSupportsItem
isSupported={modelInfo.supportsImages ?? false}
supportsLabel="Supports images"
@ -45,38 +42,37 @@ export const ModelInfoView = ({
/>
),
modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && (
<span key="maxTokens">
<span style={{ fontWeight: 500 }}>Max output:</span> {modelInfo.maxTokens?.toLocaleString()} tokens
</span>
<>
<span className="font-medium">Max output:</span> {modelInfo.maxTokens?.toLocaleString()} tokens
</>
),
modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
<span key="inputPrice">
<span style={{ fontWeight: 500 }}>Input price:</span> {formatPrice(modelInfo.inputPrice)}/million tokens
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
<span key="cacheWritesPrice">
<span style={{ fontWeight: 500 }}>Cache writes price:</span>{" "}
{formatPrice(modelInfo.cacheWritesPrice || 0)}/million tokens
</span>
),
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (
<span key="cacheReadsPrice">
<span style={{ fontWeight: 500 }}>Cache reads price:</span>{" "}
{formatPrice(modelInfo.cacheReadsPrice || 0)}/million tokens
</span>
<>
<span className="font-medium">Input price:</span> {formatPrice(modelInfo.inputPrice)} / 1M tokens
</>
),
modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
<span key="outputPrice">
<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million
tokens
</span>
<>
<span className="font-medium">Output price:</span> {formatPrice(modelInfo.outputPrice)} / 1M tokens
</>
),
modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && (
<>
<span className="font-medium">Cache reads price:</span> {formatPrice(modelInfo.cacheReadsPrice || 0)} /
1M tokens
</>
),
modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
<>
<span className="font-medium">Cache writes price:</span> {formatPrice(modelInfo.cacheWritesPrice || 0)}{" "}
/ 1M tokens
</>
),
isGemini && (
<span key="geminiInfo" style={{ fontStyle: "italic" }}>
<span className="italic">
* Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute.
After that, billing depends on prompt size.{" "}
<VSCodeLink href="https://ai.google.dev/pricing" style={{ display: "inline", fontSize: "inherit" }}>
<VSCodeLink href="https://ai.google.dev/pricing" className="text-sm">
For more info, see pricing details.
</VSCodeLink>
</span>
@ -84,14 +80,21 @@ export const ModelInfoView = ({
].filter(Boolean)
return (
<div style={{ fontSize: "12px", marginTop: "2px", color: "var(--vscode-descriptionForeground)" }}>
{infoItems.map((item, index) => (
<Fragment key={index}>
{item}
{index < infoItems.length - 1 && <br />}
</Fragment>
))}
</div>
<>
{modelInfo.description && (
<ModelDescriptionMarkdown
key="description"
markdown={modelInfo.description}
isExpanded={isDescriptionExpanded}
setIsExpanded={setIsDescriptionExpanded}
/>
)}
<div className="text-sm text-vscode-descriptionForeground">
{infoItems.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
</>
)
}
@ -104,21 +107,12 @@ const ModelInfoSupportsItem = ({
supportsLabel: string
doesNotSupportLabel: string
}) => (
<span
style={{
fontWeight: 500,
color: isSupported ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)",
}}>
<i
className={`codicon codicon-${isSupported ? "check" : "x"}`}
style={{
marginRight: 4,
marginBottom: isSupported ? 1 : -1,
fontSize: isSupported ? 11 : 13,
fontWeight: 700,
display: "inline-block",
verticalAlign: "bottom",
}}></i>
<div
className={cn(
"flex items-center gap-1 font-medium",
isSupported ? "text-vscode-charts-green" : "text-vscode-errorForeground",
)}>
<span className={cn("codicon", isSupported ? "codicon-check" : "codicon-x")} />
{isSupported ? supportsLabel : doesNotSupportLabel}
</span>
</div>
)

View file

@ -72,23 +72,20 @@ export const ModelPicker = ({
return (
<>
<div className="font-semibold">Model</div>
<Combobox type="single" inputValue={inputValue} onInputValueChange={onSelect}>
<ComboboxInput placeholder="Search model..." data-testid="model-input" />
<ComboboxContent>
<ComboboxEmpty>No model found.</ComboboxEmpty>
{modelIds.map((model) => (
<ComboboxItem key={model} value={model}>
{model}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>
<ThinkingBudget
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
<div>
<div className="font-medium">Model</div>
<Combobox type="single" inputValue={inputValue} onInputValueChange={onSelect}>
<ComboboxInput placeholder="Search model..." data-testid="model-input" />
<ComboboxContent>
<ComboboxEmpty>No model found.</ComboboxEmpty>
{modelIds.map((model) => (
<ComboboxItem key={model} value={model}>
{model}
</ComboboxItem>
))}
</ComboboxContent>
</Combobox>
</div>
{selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
<ModelInfoView
selectedModelId={selectedModelId}
@ -97,15 +94,22 @@ export const ModelPicker = ({
setIsDescriptionExpanded={setIsDescriptionExpanded}
/>
)}
<p>
<ThinkingBudget
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
/>
<div className="text-sm text-vscode-descriptionForeground">
The extension automatically fetches the latest list of models available on{" "}
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href={serviceUrl}>
{serviceName}.
<VSCodeLink href={serviceUrl} className="text-sm">
{serviceName}
</VSCodeLink>
. If you're unsure which model to choose, Roo Code works best with{" "}
<VSCodeLink onClick={() => onSelect(defaultModelId)} className="text-sm">
{defaultModelId}.
</VSCodeLink>
If you're unsure which model to choose, Roo Code works best with{" "}
<VSCodeLink onClick={() => onSelect(defaultModelId)}>{defaultModelId}.</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</p>
</div>
</>
)
}

View file

@ -8,7 +8,12 @@ type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
}
export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => (
<div className={cn("sticky top-0 z-10 bg-vscode-panel-border px-5 py-4", className)} {...props}>
<div
className={cn(
"sticky top-0 z-10 text-vscode-sideBar-foreground bg-vscode-sideBar-background brightness-90 px-5 py-4",
className,
)}
{...props}>
<h4 className="m-0">{children}</h4>
{description && <p className="text-vscode-descriptionForeground text-sm mt-2 mb-0">{description}</p>}
</div>

View file

@ -7,6 +7,7 @@ import { ApiConfiguration } from "../../../../src/shared/api"
import { vscode } from "@/utils/vscode"
import { ExtensionStateContextType, useExtensionState } from "@/context/ExtensionStateContext"
import { cn } from "@/lib/utils"
import {
AlertDialog,
AlertDialogContent,
@ -247,14 +248,13 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<h3 className="text-vscode-foreground m-0">Settings</h3>
<div className="hidden [@media(min-width:430px)]:flex items-center">
<div className="hidden [@media(min-width:400px)]:flex items-center">
{sections.map(({ id, icon: Icon, ref }) => (
<Button
key={id}
variant="ghost"
size="icon"
className={activeSection === id ? "opacity-100" : "opacity-40"}
onClick={() => scrollToSection(ref)}>
onClick={() => scrollToSection(ref)}
className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
<Icon />
</Button>
))}
@ -287,7 +287,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
</div>
<div
className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-panel-border"
className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-sideBar-background"
onScroll={handleScroll}>
<div ref={providersRef}>
<SectionHeader>

View file

@ -20,30 +20,30 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
}, [value])
return (
<div>
<VSCodeCheckbox
checked={isCustomTemperature}
onChange={(e: any) => {
const isChecked = e.target.checked
setIsCustomTemperature(isChecked)
if (!isChecked) {
setInputValue(undefined) // Unset the temperature
} else {
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
}
}}>
<span className="font-medium">Use custom temperature</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
Controls randomness in the model's responses.
</p>
<>
<div>
<VSCodeCheckbox
checked={isCustomTemperature}
onChange={(e: any) => {
const isChecked = e.target.checked
setIsCustomTemperature(isChecked)
if (!isChecked) {
setInputValue(undefined) // Unset the temperature
} else {
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
}
}}>
<span className="font-medium">Use custom temperature</span>
</VSCodeCheckbox>
<div className="text-sm text-vscode-descriptionForeground">
Controls randomness in the model's responses.
</div>
</div>
{isCustomTemperature && (
<div
style={{
marginTop: 5,
marginBottom: 10,
marginLeft: 0,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
@ -64,6 +64,6 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
</p>
</div>
)}
</div>
</>
)
}

View file

@ -35,8 +35,8 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
}
return (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1 mt-2">
<>
<div className="flex flex-col gap-1">
<div className="font-medium">Max Tokens</div>
<div className="flex items-center gap-1">
<Slider
@ -49,7 +49,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
<div className="w-12 text-sm text-center">{tokens}</div>
</div>
</div>
<div className="flex flex-col gap-1 mt-2">
<div className="flex flex-col gap-1">
<div className="font-medium">Max Thinking Tokens</div>
<div className="flex items-center gap-1">
<Slider
@ -62,6 +62,6 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
<div className="w-12 text-sm text-center">{thinkingTokens}</div>
</div>
</div>
</div>
</>
)
}

View file

@ -1,6 +1,9 @@
// npx jest src/components/settings/__tests__/ApiOptions.test.ts
import { render, screen } from "@testing-library/react"
import ApiOptions from "../ApiOptions"
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
import ApiOptions from "../ApiOptions"
// Mock VSCode components
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
@ -13,6 +16,7 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeLink: ({ children, href }: any) => <a href={href}>{children}</a>,
VSCodeRadio: ({ children, value, checked }: any) => <input type="radio" value={value} checked={checked} />,
VSCodeRadioGroup: ({ children }: any) => <div>{children}</div>,
VSCodeButton: ({ children }: any) => <div>{children}</div>,
}))
// Mock other components

View file

@ -100,6 +100,17 @@
--color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground);
--color-vscode-panel-border: var(--vscode-panel-border);
--color-vscode-sideBar-foreground: var(--vscode-sideBar-foreground);
--color-vscode-sideBar-background: var(--vscode-sideBar-background);
--color-vscode-sideBar-border: var(--vscode-sideBar-border);
--color-vscode-sideBarSectionHeader-foreground: var(--vscode-sideBarSectionHeader-foreground);
--color-vscode-sideBarSectionHeader-background: var(--vscode-sideBarSectionHeader-background);
--color-vscode-sideBarSectionHeader-border: var(--vscode-sideBarSectionHeader-border);
--color-vscode-charts-green: var(--vscode-charts-green);
--color-vscode-charts-yellow: var(--vscode-charts-yellow);
}
@layer base {