diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 1096210f842..229eba570e1 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -136,6 +136,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, ...overrides, + leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default', appFontFamily, agentStatusHooksEnabled, tabAutoGenerateTitle diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 21f1335010f..79276efc33b 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -140,6 +140,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, ...overrides, + leftSidebarAppearanceMode: overrides.leftSidebarAppearanceMode ?? 'default', appFontFamily, agentStatusHooksEnabled, tabAutoGenerateTitle diff --git a/src/main/persistence.ts b/src/main/persistence.ts index a1e7ea44acf..526f6b39f1e 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -97,6 +97,11 @@ import { normalizeOpenInApplications } from '../shared/open-in-applications' import { normalizeTerminalShortcutPolicy } from '../shared/keybindings' import { normalizeAppIconId } from '../shared/app-icon' import { normalizeTerminalCustomThemes } from '../shared/terminal-custom-themes' +import { + normalizeLeftSidebarAppearanceMode, + normalizeLeftSidebarTintColor, + normalizeLeftSidebarTintOpacity +} from '../shared/left-sidebar-appearance' import { compareFeatureInteractionUsageBuckets, getFeatureInteractionCategory, @@ -2308,6 +2313,15 @@ export class Store { terminalCustomThemes: normalizeTerminalCustomThemes( parsed.settings?.terminalCustomThemes ), + leftSidebarAppearanceMode: normalizeLeftSidebarAppearanceMode( + parsed.settings?.leftSidebarAppearanceMode + ), + leftSidebarTintColor: normalizeLeftSidebarTintColor( + parsed.settings?.leftSidebarTintColor + ), + leftSidebarTintOpacity: normalizeLeftSidebarTintOpacity( + parsed.settings?.leftSidebarTintOpacity + ), appIcon: normalizeAppIconId(parsed.settings?.appIcon), uiLanguage: normalizeUiLanguage(parsed.settings?.uiLanguage), defaultTaskSource: taskProviderSettings.defaultTaskSource, @@ -3635,6 +3649,21 @@ export class Store { updates.terminalCustomThemes ) } + if ('leftSidebarAppearanceMode' in updates) { + sanitizedUpdates.leftSidebarAppearanceMode = normalizeLeftSidebarAppearanceMode( + updates.leftSidebarAppearanceMode + ) + } + if ('leftSidebarTintColor' in updates) { + sanitizedUpdates.leftSidebarTintColor = normalizeLeftSidebarTintColor( + updates.leftSidebarTintColor + ) + } + if ('leftSidebarTintOpacity' in updates) { + sanitizedUpdates.leftSidebarTintOpacity = normalizeLeftSidebarTintOpacity( + updates.leftSidebarTintOpacity + ) + } if ('visibleTaskProviders' in updates || 'defaultTaskSource' in updates) { const taskProviderSettings = normalizeTaskProviderSettings({ visibleTaskProviders: diff --git a/src/renderer/src/components/settings/AppearancePane.test.tsx b/src/renderer/src/components/settings/AppearancePane.test.tsx index 6a54b0c63e9..39105053938 100644 --- a/src/renderer/src/components/settings/AppearancePane.test.tsx +++ b/src/renderer/src/components/settings/AppearancePane.test.tsx @@ -194,6 +194,27 @@ describe('AppearancePane', () => { expect(updateSettings).toHaveBeenCalledWith({ uiLanguage: 'zh' }) }) + it('updates the left sidebar appearance from sidebar settings', async () => { + mocks.state.settingsSearchQuery = 'left sidebar' + const updateSettings = vi.fn() + const settings = getDefaultSettings('/tmp') + + const container = await renderAppearancePane(settings, updateSettings) + const matchTerminalButton = Array.from( + container.querySelectorAll('button[role="radio"]') + ).find((button) => button.textContent === 'Match Terminal') + + expect(matchTerminalButton).toBeDefined() + + await act(async () => { + matchTerminalButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(updateSettings).toHaveBeenCalledWith({ + leftSidebarAppearanceMode: 'match-terminal' + }) + }) + it('restores the Automations sidebar button from the sidebar settings switch', async () => { const updateSettings = vi.fn() const settings = { diff --git a/src/renderer/src/components/settings/AppearancePane.tsx b/src/renderer/src/components/settings/AppearancePane.tsx index 18ea6b1762f..b1ff734d116 100644 --- a/src/renderer/src/components/settings/AppearancePane.tsx +++ b/src/renderer/src/components/settings/AppearancePane.tsx @@ -27,6 +27,7 @@ import { getAppearancePaneSearchEntries, getLanguageEntries, getLayoutEntries, + getLeftSidebarAppearanceEntry, getSidebarEntries, getStatusBarEntries, getStatusBarToggles, @@ -48,6 +49,7 @@ import { } from '@/i18n/supported-languages' import { translate } from '@/i18n/i18n' import type { UiLanguage } from '../../../../shared/ui-language' +import { LeftSidebarAppearanceSetting } from './LeftSidebarAppearanceSetting' export { getAppearancePaneSearchEntries } type AppearancePaneProps = { @@ -104,6 +106,7 @@ export function AppearancePane({ const terminalAppearanceSearchEntries = getTerminalAppearanceSearchEntries({ showWarpImport: !isWebClientLocation() }) + const leftSidebarAppearanceEntry = getLeftSidebarAppearanceEntry() const visibleSections = [ matchesSettingsSearch(searchQuery, getThemeEntries()) || (SHOW_UI_LANGUAGE_SETTING && matchesSettingsSearch(searchQuery, getLanguageEntries())) || @@ -412,6 +415,15 @@ export function AppearancePane({ />
+ + + + ) => void +} + +export function LeftSidebarAppearanceSetting({ + settings, + updateSettings +}: LeftSidebarAppearanceSettingProps): React.JSX.Element { + return ( +
+ + size="sm" + value={settings.leftSidebarAppearanceMode ?? 'default'} + onChange={(leftSidebarAppearanceMode) => updateSettings({ leftSidebarAppearanceMode })} + ariaLabel={translate( + 'auto.components.settings.AppearancePane.leftSidebarAppearance.title', + 'Left Sidebar Appearance' + )} + options={[ + { + value: 'default', + label: translate( + 'auto.components.settings.AppearancePane.leftSidebarAppearance.default', + 'Default' + ) + }, + { + value: 'match-terminal', + label: translate( + 'auto.components.settings.AppearancePane.leftSidebarAppearance.matchTerminal', + 'Match Terminal' + ) + }, + { + value: 'soft-contrast', + label: translate( + 'auto.components.settings.AppearancePane.leftSidebarAppearance.softContrast', + 'Soft Contrast' + ) + }, + { + value: 'tinted', + label: translate( + 'auto.components.settings.AppearancePane.leftSidebarAppearance.tinted', + 'Tinted' + ) + } + ]} + /> + } + /> + {(settings.leftSidebarAppearanceMode ?? 'default') === 'tinted' ? ( +
+ updateSettings({ leftSidebarTintColor })} + /> + updateSettings({ leftSidebarTintOpacity })} + /> +
+ ) : null} +
+ ) +} diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index cc4d57cd6d0..cd7f1bea329 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -947,6 +947,7 @@ function Settings(): React.JSX.Element { > 0} diff --git a/src/renderer/src/components/settings/SettingsSidebar.test.tsx b/src/renderer/src/components/settings/SettingsSidebar.test.tsx index ddcec2cf6eb..11ef40454e7 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.test.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.test.tsx @@ -1,9 +1,11 @@ import { renderToStaticMarkup } from 'react-dom/server' import { Bot, Mic, Network } from 'lucide-react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' import { SettingsSidebar } from './SettingsSidebar' import { TooltipProvider } from '../ui/tooltip' import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress' +import type { GlobalSettings } from '../../../../shared/types' const mocks = vi.hoisted(() => ({ useSettingsSetupGuideProgress: vi.fn() @@ -29,11 +31,15 @@ function makeSetupGuideProgress( } } -function renderSidebar(activeSectionId = 'orchestration'): string { +function renderSidebar( + activeSectionId = 'orchestration', + settings: GlobalSettings = getDefaultSettings('/tmp') +): string { return renderToStaticMarkup( { mocks.useSettingsSetupGuideProgress.mockReturnValue(makeSetupGuideProgress()) }) + it('applies left sidebar appearance styles to the settings navigation', () => { + const markup = renderSidebar('orchestration', { + ...getDefaultSettings('/tmp'), + leftSidebarAppearanceMode: 'match-terminal', + terminalColorOverrides: { + background: '#101820', + foreground: '#f0f4f8' + } + }) + + expect(markup).toContain('--worktree-sidebar:#101820') + expect(markup).toContain('--worktree-sidebar-foreground:#f0f4f8') + }) + it('renders install state labels separately from static badges', () => { const markup = renderSidebar() diff --git a/src/renderer/src/components/settings/SettingsSidebar.tsx b/src/renderer/src/components/settings/SettingsSidebar.tsx index 6fa13381a67..4a28cca7566 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.tsx @@ -1,8 +1,9 @@ -import type { RefObject } from 'react' +import type { CSSProperties, RefObject } from 'react' +import { useMemo } from 'react' import { ArrowLeft, Search, Server } from 'lucide-react' import type { RepoIcon } from '../../../../shared/repo-icon' import type { SettingsNavIcon, SettingsNavInstallStatus } from '@/lib/settings-navigation-types' -import type { GitHubRepositoryIdentity } from '../../../../shared/types' +import type { GitHubRepositoryIdentity, GlobalSettings } from '../../../../shared/types' import { useShortcutLabel } from '@/hooks/useShortcutLabel' import { cn } from '@/lib/utils' import { RepoIconGlyph } from '../repo/repo-icon' @@ -13,6 +14,8 @@ import { SetupGuideProgressRing } from '../setup-guide/SetupGuideProgressRing' import { useSettingsSetupGuideProgress } from './settings-setup-guide-progress' import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress' import { translate } from '@/i18n/i18n' +import { resolveLeftSidebarStyleVariables } from '@/lib/left-sidebar-appearance' +import { useSystemPrefersDark } from '../terminal-pane/use-system-prefers-dark' type NavSection = { id: string @@ -37,6 +40,7 @@ type RepoNavSection = NavSection & { type SettingsSidebarProps = { activeSectionId: string + settings: GlobalSettings | null generalGroups: NavGroup[] repoSections: RepoNavSection[] hasRepos: boolean @@ -112,6 +116,7 @@ function SettingsSetupGuideNavRow({ export function SettingsSidebar({ activeSectionId, + settings, generalGroups, repoSections, hasRepos, @@ -122,6 +127,11 @@ export function SettingsSidebar({ onSelectSection }: SettingsSidebarProps): React.JSX.Element { const setupGuideProgress = useSettingsSetupGuideProgress(true) + const systemPrefersDark = useSystemPrefersDark() + const leftSidebarStyle = useMemo( + () => resolveLeftSidebarStyleVariables(settings, systemPrefersDark), + [settings, systemPrefersDark] + ) as CSSProperties | undefined const setupActive = activeSectionId === 'setup-guide' // Why: "Hide from sidebar" only hides the top-left app sidebar prompt; // Settings should remain a stable place to reopen the checklist. @@ -159,7 +169,10 @@ export function SettingsSidebar({ ) return ( -