diff --git a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx index f449140b..b96d2de1 100644 --- a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx +++ b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx @@ -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 + + {/* Sidebar Style Section */} +
+
+ + +
+

+ Choose between a modern unified sidebar or classic Discord-style layout with a separate + project switcher. +

+ +
+ {/* Unified Sidebar Option */} + + + {/* Discord-style Sidebar Option */} + +
+
); diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 7398aece..8f24b67c 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -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, diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 8c7d9961..15d781d9 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -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 { useAppStore.setState({ theme: serverSettings.theme as unknown as ThemeMode, sidebarOpen: serverSettings.sidebarOpen, + sidebarStyle: serverSettings.sidebarStyle ?? 'unified', chatHistoryOpen: serverSettings.chatHistoryOpen, maxConcurrency: serverSettings.maxConcurrency, autoModeByWorktree: restoredAutoModeByWorktree, diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index f374b7dd..1bb006c5 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -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' && }
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()((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 }), diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 87975a81..a4a7635e 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -145,6 +145,7 @@ export { DEFAULT_PROMPT_CUSTOMIZATION } from './prompts.js'; // Settings types and constants export type { ThemeMode, + SidebarStyle, PlanningMode, ThinkingLevel, ServerLogLevel, diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index e04110c5..67b1b7b1 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -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,