feat: Add sidebar style options to appearance settings

- Introduced a new section in the Appearance settings to allow users to choose between 'unified' and 'discord' sidebar layouts.
- Updated the app state and settings migration to include the new sidebarStyle property.
- Enhanced the UI to reflect the selected sidebar style with appropriate visual feedback.
- Ensured synchronization of sidebar style settings across the application.
This commit is contained in:
Shirone
2026-01-23 16:34:44 +01:00
parent 0b92349890
commit 4012a2964a
7 changed files with 122 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Label } from '@/components/ui/label';
import { Palette, Moon, Sun, Type } from 'lucide-react';
import { Palette, Moon, Sun, Type, PanelLeft, Columns2 } from 'lucide-react';
import { darkThemes, lightThemes } from '@/config/theme-options';
import {
UI_SANS_FONT_OPTIONS,
@@ -11,6 +11,7 @@ import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { FontSelector } from '@/components/shared';
import type { Theme } from '../shared/types';
import type { SidebarStyle } from '@automaker/types';
interface AppearanceSectionProps {
effectiveTheme: Theme;
@@ -18,7 +19,14 @@ interface AppearanceSectionProps {
}
export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceSectionProps) {
const { fontFamilySans, fontFamilyMono, setFontSans, setFontMono } = useAppStore();
const {
fontFamilySans,
fontFamilyMono,
setFontSans,
setFontMono,
sidebarStyle,
setSidebarStyle,
} = useAppStore();
// Determine if current theme is light or dark
const isLightTheme = lightThemes.some((t) => t.value === effectiveTheme);
@@ -189,6 +197,94 @@ export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceS
</div>
</div>
</div>
{/* Sidebar Style Section */}
<div className="space-y-4 pt-6 border-t border-border/50">
<div className="flex items-center gap-2 mb-4">
<PanelLeft className="w-4 h-4 text-muted-foreground" />
<Label className="text-foreground font-medium">Sidebar Layout</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2 mb-4">
Choose between a modern unified sidebar or classic Discord-style layout with a separate
project switcher.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Unified Sidebar Option */}
<button
onClick={() => setSidebarStyle('unified')}
className={cn(
'group flex flex-col items-center gap-3 p-4 rounded-xl',
'text-sm font-medium transition-all duration-200 ease-out',
sidebarStyle === 'unified'
? [
'bg-gradient-to-br from-brand-500/15 to-brand-600/10',
'border-2 border-brand-500/40',
'text-foreground',
'shadow-md shadow-brand-500/10',
]
: [
'bg-accent/30 hover:bg-accent/50',
'border border-border/50 hover:border-border',
'text-muted-foreground hover:text-foreground',
'hover:shadow-sm',
],
'hover:scale-[1.02] active:scale-[0.98]'
)}
data-testid="sidebar-style-unified"
>
<PanelLeft
className={cn(
'w-8 h-8 transition-all duration-200',
sidebarStyle === 'unified' ? 'text-brand-500' : 'text-muted-foreground'
)}
/>
<div className="text-center">
<div className="font-medium">Unified</div>
<div className="text-xs text-muted-foreground mt-1">
Single sidebar with project dropdown
</div>
</div>
</button>
{/* Discord-style Sidebar Option */}
<button
onClick={() => setSidebarStyle('discord')}
className={cn(
'group flex flex-col items-center gap-3 p-4 rounded-xl',
'text-sm font-medium transition-all duration-200 ease-out',
sidebarStyle === 'discord'
? [
'bg-gradient-to-br from-brand-500/15 to-brand-600/10',
'border-2 border-brand-500/40',
'text-foreground',
'shadow-md shadow-brand-500/10',
]
: [
'bg-accent/30 hover:bg-accent/50',
'border border-border/50 hover:border-border',
'text-muted-foreground hover:text-foreground',
'hover:shadow-sm',
],
'hover:scale-[1.02] active:scale-[0.98]'
)}
data-testid="sidebar-style-discord"
>
<Columns2
className={cn(
'w-8 h-8 transition-all duration-200',
sidebarStyle === 'discord' ? 'text-brand-500' : 'text-muted-foreground'
)}
/>
<div className="text-center">
<div className="font-medium">Classic</div>
<div className="text-xs text-muted-foreground mt-1">
Separate project switcher + sidebar
</div>
</div>
</button>
</div>
</div>
</div>
</div>
);

View File

@@ -698,6 +698,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
fontFamilySans: settings.fontFamilySans ?? null,
fontFamilyMono: settings.fontFamilyMono ?? null,
sidebarOpen: settings.sidebarOpen ?? true,
sidebarStyle: settings.sidebarStyle ?? 'unified',
chatHistoryOpen: settings.chatHistoryOpen ?? false,
maxConcurrency: settings.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
autoModeByWorktree: restoredAutoModeByWorktree,

View File

@@ -53,6 +53,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
'terminalFontFamily', // Maps to terminalState.fontFamily
'openTerminalMode', // Maps to terminalState.openTerminalMode
'sidebarOpen',
'sidebarStyle',
'chatHistoryOpen',
'maxConcurrency',
'autoModeByWorktree', // Per-worktree auto mode settings (only maxConcurrency is persisted)
@@ -697,6 +698,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
useAppStore.setState({
theme: serverSettings.theme as unknown as ThemeMode,
sidebarOpen: serverSettings.sidebarOpen,
sidebarStyle: serverSettings.sidebarStyle ?? 'unified',
chatHistoryOpen: serverSettings.chatHistoryOpen,
maxConcurrency: serverSettings.maxConcurrency,
autoModeByWorktree: restoredAutoModeByWorktree,

View File

@@ -4,6 +4,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { createLogger } from '@automaker/utils/logger';
import { Sidebar } from '@/components/layout/sidebar';
import { ProjectSwitcher } from '@/components/layout/project-switcher';
import {
FileBrowserProvider,
useFileBrowser,
@@ -167,6 +168,7 @@ function RootLayoutContent() {
theme,
fontFamilySans,
fontFamilyMono,
sidebarStyle,
skipSandboxWarning,
setSkipSandboxWarning,
fetchCodexModels,
@@ -860,6 +862,8 @@ function RootLayoutContent() {
aria-hidden="true"
/>
)}
{/* Discord-style layout: narrow project switcher + expandable sidebar */}
{sidebarStyle === 'discord' && <ProjectSwitcher />}
<Sidebar />
<div
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"

View File

@@ -36,6 +36,7 @@ import type {
EventHook,
ClaudeApiProfile,
ClaudeCompatibleProvider,
SidebarStyle,
} from '@automaker/types';
import {
getAllCursorModelIds,
@@ -610,6 +611,7 @@ export interface AppState {
// View state
currentView: ViewMode;
sidebarOpen: boolean;
sidebarStyle: SidebarStyle; // 'unified' (modern) or 'discord' (classic two-sidebar layout)
mobileSidebarHidden: boolean; // Completely hides sidebar on mobile
// Agent Session state (per-project, keyed by project path)
@@ -1046,6 +1048,7 @@ export interface AppActions {
setCurrentView: (view: ViewMode) => void;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setSidebarStyle: (style: SidebarStyle) => void;
toggleMobileSidebarHidden: () => void;
setMobileSidebarHidden: (hidden: boolean) => void;
@@ -1471,6 +1474,7 @@ const initialState: AppState = {
projectHistoryIndex: -1,
currentView: 'welcome',
sidebarOpen: true,
sidebarStyle: 'unified', // Default to modern unified sidebar
mobileSidebarHidden: false, // Sidebar visible by default on mobile
lastSelectedSessionByProject: {},
theme: getStoredTheme() || 'dark', // Use localStorage theme as initial value, fallback to 'dark'
@@ -1929,6 +1933,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setCurrentView: (view) => set({ currentView: view }),
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setSidebarStyle: (style) => set({ sidebarStyle: style }),
toggleMobileSidebarHidden: () => set({ mobileSidebarHidden: !get().mobileSidebarHidden }),
setMobileSidebarHidden: (hidden) => set({ mobileSidebarHidden: hidden }),

View File

@@ -145,6 +145,7 @@ export { DEFAULT_PROMPT_CUSTOMIZATION } from './prompts.js';
// Settings types and constants
export type {
ThemeMode,
SidebarStyle,
PlanningMode,
ThinkingLevel,
ServerLogLevel,

View File

@@ -78,6 +78,14 @@ export type ServerLogLevel = 'error' | 'warn' | 'info' | 'debug';
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
/**
* SidebarStyle - Sidebar layout style options
*
* - 'unified': Single sidebar with integrated project dropdown (default, modern)
* - 'discord': Two sidebars - narrow project switcher + expandable navigation sidebar (classic)
*/
export type SidebarStyle = 'unified' | 'discord';
/**
* Thinking token budget mapping based on Claude SDK documentation.
* @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
@@ -836,6 +844,8 @@ export interface GlobalSettings {
// UI State Preferences
/** Whether sidebar is currently open */
sidebarOpen: boolean;
/** Sidebar layout style ('unified' = modern single sidebar, 'discord' = classic two-sidebar layout) */
sidebarStyle: SidebarStyle;
/** Whether chat history panel is open */
chatHistoryOpen: boolean;
@@ -1310,6 +1320,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
skipClaudeSetup: false,
theme: 'dark',
sidebarOpen: true,
sidebarStyle: 'unified',
chatHistoryOpen: false,
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
defaultSkipTests: true,