diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 579356ae2d45..10effdb2c65e 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -184,6 +184,7 @@ export const globalSettingsSchema = z.object({ hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), lastModeImportPath: z.string().optional(), + accentColor: z.string().optional(), }) export type GlobalSettings = z.infer diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 0a4bd9abb90c..14608c7a52aa 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1692,6 +1692,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true) // No need to call postStateToWebview here as the UI already updated optimistically break + case "setAccentColor": + await updateGlobalState("accentColor", message.text ?? undefined) + await provider.postStateToWebview() + break case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index f10808cd428d..4482cfacb337 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -199,6 +199,7 @@ export interface WebviewMessage { | "profileThresholds" | "setHistoryPreviewCollapsed" | "setReasoningBlockCollapsed" + | "setAccentColor" | "openExternal" | "filterMarketplaceItems" | "marketplaceButtonClicked" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 220c8cf3af2b..e17245177c64 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -79,6 +79,7 @@ const App = () => { cloudOrganizations, renderContext, mdmCompliant, + accentColor, } = useExtensionState() // Create a persistent state manager @@ -233,6 +234,18 @@ const App = () => { } }, [renderContext]), ) + + // Apply accent color to CSS custom property + useEffect(() => { + if (accentColor) { + document.documentElement.style.setProperty("--accent", accentColor) + document.documentElement.style.setProperty("--color-accent", accentColor) + } else { + // Reset to default when no accent color is set + document.documentElement.style.removeProperty("--accent") + document.documentElement.style.removeProperty("--color-accent") + } + }, [accentColor]) // Track marketplace tab views useEffect(() => { if (tab === "marketplace") { diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 506eabc4cb50..7f97233b8b87 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -791,6 +791,7 @@ const SettingsView = forwardRef(({ onDone, t {activeTab === "ui" && ( )} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 2de16e688224..19fbb08a39ce 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -1,6 +1,6 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, useState, useEffect } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { Glasses } from "lucide-react" import { telemetryClient } from "@/utils/TelemetryClient" @@ -11,11 +11,18 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext" interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean + accentColor?: string setCachedStateField: SetCachedStateField } -export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => { +export const UISettings = ({ reasoningBlockCollapsed, accentColor, setCachedStateField, ...props }: UISettingsProps) => { const { t } = useAppTranslation() + const [colorValue, setColorValue] = useState(accentColor || "") + + // Sync local state with prop changes + useEffect(() => { + setColorValue(accentColor || "") + }, [accentColor]) const handleReasoningBlockCollapsedChange = (value: boolean) => { setCachedStateField("reasoningBlockCollapsed", value) @@ -26,6 +33,20 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr }) } + const handleAccentColorChange = (value: string) => { + setColorValue(value) + // Only update if it's a valid color or empty + const trimmedValue = value.trim() + if (trimmedValue === "" || /^#[0-9A-Fa-f]{6}$/.test(trimmedValue)) { + setCachedStateField("accentColor", trimmedValue || undefined) + + // Track telemetry event + telemetryClient.capture("ui_settings_accent_color_changed", { + hasColor: trimmedValue !== "", + }) + } + } + return (
@@ -49,6 +70,33 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr {t("settings:ui.collapseThinking.description")}
+ + {/* Accent Color Setting */} +
+ +
+ handleAccentColorChange(e.target.value)} + data-testid="accent-color-input" + style={{ flex: 1 }} + /> + handleAccentColorChange(e.target.value)} + className="h-8 w-16 cursor-pointer rounded border border-vscode-input-border" + data-testid="accent-color-picker" + /> +
+
+ Customize the accent color used throughout the extension. Leave empty to use the default theme color. Enter a hex color code (e.g., #007ACC) or use the color picker. +
+
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 6443ccad93d5..119b0f90ed91 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -165,6 +165,8 @@ export interface ExtensionStateContextType extends ExtensionState { setIncludeCurrentTime: (value: boolean) => void includeCurrentCost?: boolean setIncludeCurrentCost: (value: boolean) => void + accentColor?: string + setAccentColor: (value: string | undefined) => void } export const ExtensionStateContext = createContext(undefined) @@ -298,6 +300,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false) const [includeCurrentTime, setIncludeCurrentTime] = useState(true) const [includeCurrentCost, setIncludeCurrentCost] = useState(true) + const [accentColor, setAccentColor] = useState(undefined) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -343,6 +346,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).includeCurrentCost !== undefined) { setIncludeCurrentCost((newState as any).includeCurrentCost) } + // Update accentColor if present in state message + if ((newState as any).accentColor !== undefined) { + setAccentColor((newState as any).accentColor) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -596,6 +603,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setIncludeCurrentTime, includeCurrentCost, setIncludeCurrentCost, + accentColor, + setAccentColor: (value: string | undefined) => { + setAccentColor(value) + vscode.postMessage({ type: "setAccentColor", text: value }) + }, } return {children}