瀏覽代碼

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

Fix settings colors for themes that use color transparency
Chris Estreich 9 月之前
父節點
當前提交
0d4a15ebab

+ 10 - 14
webview-ui/src/components/common/VSCodeButtonLink.tsx

@@ -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>
+)

文件差異過大導致無法顯示
+ 421 - 601
webview-ui/src/components/settings/ApiOptions.tsx


+ 17 - 59
webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx

@@ -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>
 		)
 	},
 )

+ 57 - 63
webview-ui/src/components/settings/ModelInfoView.tsx

@@ -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>
+			<>
+				<span className="font-medium">Input price:</span> {formatPrice(modelInfo.inputPrice)} / 1M tokens
+			</>
 		),
-		modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && (
-			<span key="cacheWritesPrice">
-				<span style={{ fontWeight: 500 }}>Cache writes price:</span>{" "}
-				{formatPrice(modelInfo.cacheWritesPrice || 0)}/million tokens
-			</span>
+		modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
+			<>
+				<span className="font-medium">Output price:</span> {formatPrice(modelInfo.outputPrice)} / 1M tokens
+			</>
 		),
 		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">Cache reads price:</span> {formatPrice(modelInfo.cacheReadsPrice || 0)} /
+				1M tokens
+			</>
 		),
-		modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && (
-			<span key="outputPrice">
-				<span style={{ fontWeight: 500 }}>Output price:</span> {formatPrice(modelInfo.outputPrice)}/million
-				tokens
-			</span>
+		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>
 )

+ 27 - 23
webview-ui/src/components/settings/ModelPicker.tsx

@@ -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>
 		</>
 	)
 }

+ 6 - 1
webview-ui/src/components/settings/SectionHeader.tsx

@@ -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>

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

@@ -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>

+ 21 - 21
webview-ui/src/components/settings/TemperatureControl.tsx

@@ -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>
+		</>
 	)
 }

+ 4 - 4
webview-ui/src/components/settings/ThinkingBudget.tsx

@@ -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>
+		</>
 	)
 }

+ 5 - 1
webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx

@@ -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

+ 11 - 0
webview-ui/src/index.css

@@ -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 {

部分文件因文件數量過多而無法顯示