From 4012a2964ac7654a4570842a6527b42dfe09665e Mon Sep 17 00:00:00 2001 From: Shirone Date: Fri, 23 Jan 2026 16:34:44 +0100 Subject: [PATCH 1/3] 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. --- .../appearance/appearance-section.tsx | 100 +++++++++++++++++- apps/ui/src/hooks/use-settings-migration.ts | 1 + apps/ui/src/hooks/use-settings-sync.ts | 2 + apps/ui/src/routes/__root.tsx | 4 + apps/ui/src/store/app-store.ts | 5 + libs/types/src/index.ts | 1 + libs/types/src/settings.ts | 11 ++ 7 files changed, 122 insertions(+), 2 deletions(-) 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, From f005c30017925417e705fd252835b92a9dc4938e Mon Sep 17 00:00:00 2001 From: Shirone Date: Fri, 23 Jan 2026 16:47:32 +0100 Subject: [PATCH 2/3] feat: Enhance sidebar navigation with collapsible sections and state management - Added support for collapsible navigation sections in the sidebar, allowing users to expand or collapse sections based on their preferences. - Integrated the collapsed state management into the app store for persistence across sessions. - Updated the sidebar component to conditionally render the header based on the selected sidebar style. - Ensured synchronization of collapsed section states with user settings for a consistent experience. --- .../sidebar/components/sidebar-navigation.tsx | 82 ++++++++++++------- .../src/components/layout/sidebar/sidebar.tsx | 19 +++-- apps/ui/src/hooks/use-settings-migration.ts | 1 + apps/ui/src/hooks/use-settings-sync.ts | 2 + apps/ui/src/store/app-store.ts | 12 +++ libs/types/src/settings.ts | 3 + 6 files changed, 83 insertions(+), 36 deletions(-) diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index e7fd179e..293fb7e4 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -1,10 +1,11 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import type { NavigateOptions } from '@tanstack/react-router'; import { ChevronDown, Wrench, Github } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { formatShortcut } from '@/store/app-store'; +import { formatShortcut, useAppStore } from '@/store/app-store'; import type { NavSection } from '../types'; import type { Project } from '@/lib/electron'; +import type { SidebarStyle } from '@automaker/types'; import { Spinner } from '@/components/ui/spinner'; import { DropdownMenu, @@ -23,6 +24,7 @@ const sectionIcons: Record> interface SidebarNavigationProps { currentProject: Project | null; sidebarOpen: boolean; + sidebarStyle: SidebarStyle; navSections: NavSection[]; isActiveRoute: (id: string) => boolean; navigate: (opts: NavigateOptions) => void; @@ -32,6 +34,7 @@ interface SidebarNavigationProps { export function SidebarNavigation({ currentProject, sidebarOpen, + sidebarStyle, navSections, isActiveRoute, navigate, @@ -39,21 +42,26 @@ export function SidebarNavigation({ }: SidebarNavigationProps) { const navRef = useRef(null); - // Track collapsed state for each collapsible section - const [collapsedSections, setCollapsedSections] = useState>({}); + // Get collapsed state from store (persisted across restarts) + const { collapsedNavSections, setCollapsedNavSections, toggleNavSection } = useAppStore(); // Initialize collapsed state when sections change (e.g., GitHub section appears) + // Only set defaults for sections that don't have a persisted state useEffect(() => { - setCollapsedSections((prev) => { - const updated = { ...prev }; - navSections.forEach((section) => { - if (section.collapsible && section.label && !(section.label in updated)) { - updated[section.label] = section.defaultCollapsed ?? false; - } - }); - return updated; + let hasNewSections = false; + const updated = { ...collapsedNavSections }; + + navSections.forEach((section) => { + if (section.collapsible && section.label && !(section.label in updated)) { + updated[section.label] = section.defaultCollapsed ?? false; + hasNewSections = true; + } }); - }, [navSections]); + + if (hasNewSections) { + setCollapsedNavSections(updated); + } + }, [navSections, collapsedNavSections, setCollapsedNavSections]); // Check scroll state const checkScrollState = useCallback(() => { @@ -77,14 +85,7 @@ export function SidebarNavigation({ nav.removeEventListener('scroll', checkScrollState); resizeObserver.disconnect(); }; - }, [checkScrollState, collapsedSections]); - - const toggleSection = useCallback((label: string) => { - setCollapsedSections((prev) => ({ - ...prev, - [label]: !prev[label], - })); - }, []); + }, [checkScrollState, collapsedNavSections]); // Filter sections: always show non-project sections, only show project sections when project exists const visibleSections = navSections.filter((section) => { @@ -97,10 +98,17 @@ export function SidebarNavigation({ }); return ( -