diff --git a/src-tauri/src/models/project.rs b/src-tauri/src/models/project.rs index 8413907..1b72c73 100644 --- a/src-tauri/src/models/project.rs +++ b/src-tauri/src/models/project.rs @@ -101,6 +101,8 @@ pub struct CodeSettings { pub theme: String, // e.g., "github" | "dracula" #[serde(default = "default_font_size")] pub font_size: u16, // in px + #[serde(default = "default_auto_collapse_sidebar")] + pub auto_collapse_sidebar: bool, } fn default_code_theme() -> String { @@ -109,12 +111,16 @@ fn default_code_theme() -> String { fn default_font_size() -> u16 { 14 } +fn default_auto_collapse_sidebar() -> bool { + false +} impl Default for CodeSettings { fn default() -> Self { Self { theme: default_code_theme(), font_size: default_font_size(), + auto_collapse_sidebar: default_auto_collapse_sidebar(), } } } diff --git a/src/App.tsx b/src/App.tsx index f6a99c6..167703f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { AppSidebar } from "@/components/app-sidebar" -import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar" +import { SidebarProvider, SidebarTrigger, SidebarInset, useSidebar } from "@/components/ui/sidebar" import { SidebarWidthProvider } from "@/contexts/sidebar-width-context" import { Separator } from "@/components/ui/separator" import { @@ -80,6 +80,24 @@ function ProjectView({ project, selectedAgent, activeTab, onTabChange }: Project ) } +function SidebarAutoCollapseManager({ activeTab, enabled, projectActive }: { activeTab: string; enabled: boolean; projectActive: boolean }) { + const { setOpen } = useSidebar() + + useEffect(() => { + setOpen((currentOpen) => { + if (!enabled || !projectActive) { + return true + } + if (activeTab === 'code') { + return false + } + return true + }) + }, [activeTab, enabled, projectActive, setOpen]) + + return null +} + function AppContent() { const { settings } = useSettings() const [isSettingsOpen, setIsSettingsOpen] = useState(false) @@ -416,6 +434,11 @@ function AppContent() { return ( + { + setSelectedFile((prev) => (prev ? null : prev)); + }, [viewScope, workspacePath]); + const handleFileSelect = (file: FileInfo) => { setSelectedFile(file); }; @@ -464,22 +468,6 @@ export function CodeView({ project, tauriInvoke }: CodeViewProps) { {/* File Explorer Sidebar */}
- - {viewScope !== 'workspace' ? ( - - ) : ( -
- Workspace View -
- )} {viewScope !== 'workspace' && ( + ) + } + + const TabsContent = ({ value, children, forceMount, ...props }: any) => { + const context = React.useContext(TabsContext) + if (!context) { + throw new Error('TabsContent must be used within Tabs') + } + if (!forceMount && context.value !== value) return null + return ( +
+ {children} +
+ ) + } + + return { Tabs, TabsList, TabsTrigger, TabsContent } +}) + +let autoCollapse = true + +const buildSettings = () => ({ + show_console_output: true, + projects_folder: '', + file_mentions_enabled: true, + show_welcome_recent_projects: true, + chat_send_shortcut: 'mod+enter' as const, + ui_theme: 'auto', + default_cli_agent: 'claude' as const, + code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: autoCollapse }, +}) + +if (typeof window !== 'undefined' && !window.matchMedia) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => ({ + matches: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })), + }) +} + +if (typeof document !== 'undefined') describe('App sidebar auto-collapse in code view', () => { + beforeEach(() => { + const invoke = tauriCore.invoke as unknown as ReturnType + invoke.mockReset() + autoCollapse = true + invoke.mockImplementation(async (cmd: string) => { + switch (cmd) { + case 'load_app_settings': + return buildSettings() + case 'list_recent_projects': + return [project] + case 'refresh_recent_projects': + return [project] + case 'open_existing_project': + return project + case 'get_cli_project_path': + return null + case 'clear_cli_project_path': + return null + case 'get_user_home_directory': + return '/projects' + case 'set_window_theme': + case 'add_project_to_recent': + case 'save_app_settings': + return null + default: + return null + } + }) + }) + + it('collapses the sidebar when entering the Code tab and restores it afterwards', async () => { + render() + + const projectButton = await screen.findByRole('button', { name: /Sample Project/i }) + fireEvent.click(projectButton) + + const sidebarPanel = await screen.findByTestId('app-sidebar') + const sidebar = sidebarPanel.closest('[data-state]') as HTMLElement | null + expect(sidebar).not.toBeNull() + await waitFor(() => expect(sidebar).toHaveAttribute('data-state', 'expanded')) + + const codeTab = screen.getByRole('tab', { name: /Code/i }) + fireEvent.click(codeTab) + + await waitFor(() => expect(sidebar).toHaveAttribute('data-state', 'collapsed')) + + const chatTab = screen.getByRole('tab', { name: /Chat/i }) + fireEvent.click(chatTab) + + await waitFor(() => expect(sidebar).toHaveAttribute('data-state', 'expanded')) + }) + + it('keeps sidebar expanded when preference is disabled', async () => { + autoCollapse = false + render() + + const projectButton = await screen.findByRole('button', { name: /Sample Project/i }) + fireEvent.click(projectButton) + + const sidebarPanel = await screen.findByTestId('app-sidebar') + const sidebar = sidebarPanel.closest('[data-state]') as HTMLElement | null + expect(sidebar).not.toBeNull() + await waitFor(() => expect(sidebar).toHaveAttribute('data-state', 'expanded')) + + const codeTab = screen.getByRole('tab', { name: /Code/i }) + fireEvent.click(codeTab) + + await waitFor(() => expect(sidebar).toHaveAttribute('data-state', 'expanded')) + }) +}) diff --git a/src/components/__tests__/App.welcome.recents.test.tsx b/src/components/__tests__/App.welcome.recents.test.tsx index 59bfb43..72f0566 100644 --- a/src/components/__tests__/App.welcome.recents.test.tsx +++ b/src/components/__tests__/App.welcome.recents.test.tsx @@ -19,7 +19,7 @@ const { RECENT_PROJECTS, defaultInvokeImplementation } = vi.hoisted(() => { const handler = async (cmd: string) => { switch (cmd) { case 'load_app_settings': - return { show_welcome_recent_projects: true, show_console_output: true, file_mentions_enabled: true, projects_folder: '', ui_theme: 'auto', chat_send_shortcut: 'mod+enter', code_settings: { theme: 'github', font_size: 14 } } + return { show_welcome_recent_projects: true, show_console_output: true, file_mentions_enabled: true, projects_folder: '', ui_theme: 'auto', chat_send_shortcut: 'mod+enter', code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false } } case 'list_recent_projects': return recents case 'refresh_recent_projects': @@ -167,7 +167,7 @@ if (typeof document !== 'undefined') describe('App welcome screen recent project ;(invoke as any).mockImplementation(async (cmd: string) => { switch (cmd) { case 'load_app_settings': - return { show_welcome_recent_projects: false, show_console_output: true, file_mentions_enabled: true, projects_folder: '', ui_theme: 'auto', chat_send_shortcut: 'mod+enter', code_settings: { theme: 'github', font_size: 14 } } + return { show_welcome_recent_projects: false, show_console_output: true, file_mentions_enabled: true, projects_folder: '', ui_theme: 'auto', chat_send_shortcut: 'mod+enter', code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false } } case 'list_recent_projects': return [] case 'check_ai_agents': diff --git a/src/components/__tests__/CodeView.refresh.test.tsx b/src/components/__tests__/CodeView.refresh.test.tsx index 09690c6..dd3606b 100644 --- a/src/components/__tests__/CodeView.refresh.test.tsx +++ b/src/components/__tests__/CodeView.refresh.test.tsx @@ -23,7 +23,7 @@ vi.mock('@tauri-apps/api/core', () => ({ file_mentions_enabled: true, chat_send_shortcut: 'mod+enter', show_welcome_recent_projects: true, - code_settings: { theme: 'github', font_size: 14 }, + code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false }, ui_theme: 'auto', } } diff --git a/src/components/__tests__/codeview.workspaces.test.tsx b/src/components/__tests__/codeview.workspaces.test.tsx index 39a9ab2..ac5c5e7 100644 --- a/src/components/__tests__/codeview.workspaces.test.tsx +++ b/src/components/__tests__/codeview.workspaces.test.tsx @@ -23,6 +23,8 @@ if (typeof document !== 'undefined') describe('CodeView workspaces', () => { ) + expect(screen.queryByText('View')).not.toBeInTheDocument() + expect(screen.queryByText(/workspace view/i)).not.toBeInTheDocument() // Switch to workspace view automatically (since mocked list is non-empty) await waitFor(() => expect(screen.getByText('Workspace')).toBeInTheDocument()) // Open workspace select diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 9d21ac1..75c7bcb 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -68,7 +68,7 @@ export function AppSidebar({ isSettingsOpen, setIsSettingsOpen, onRefreshProject return ( - + {/* Sidebar title bar drag area - matching the main content */}
({ chat_send_shortcut: 'mod+enter', show_welcome_recent_projects: true, max_chat_history: 15, - code_settings: { theme: 'github', font_size: 14 }, + code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false }, default_cli_agent: currentDefaultAgent, }, updateSettings: vi.fn(), diff --git a/src/components/chat/__tests__/ChatInterface.historyLimit.test.tsx b/src/components/chat/__tests__/ChatInterface.historyLimit.test.tsx index ccb3ff7..137b594 100644 --- a/src/components/chat/__tests__/ChatInterface.historyLimit.test.tsx +++ b/src/components/chat/__tests__/ChatInterface.historyLimit.test.tsx @@ -55,7 +55,7 @@ const defaultInvokeImpl = async (cmd: string) => { projects_folder: '', ui_theme: 'auto', show_welcome_recent_projects: true, - code_settings: { theme: 'github', font_size: 14 }, + code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false }, } default: return null diff --git a/src/components/settings/CodeSettings.tsx b/src/components/settings/CodeSettings.tsx index 79b5372..6433c3d 100644 --- a/src/components/settings/CodeSettings.tsx +++ b/src/components/settings/CodeSettings.tsx @@ -4,21 +4,25 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useSettings } from "@/contexts/settings-context"; import { useState, useEffect } from "react"; +import { Switch } from "@/components/ui/switch"; export function CodeSettings() { const { settings, updateSettings } = useSettings(); const [tempTheme, setTempTheme] = useState(settings.code_settings.theme); const [tempFontSize, setTempFontSize] = useState(settings.code_settings.font_size); + const [tempAutoCollapse, setTempAutoCollapse] = useState(settings.code_settings.auto_collapse_sidebar); const [isSaving, setIsSaving] = useState(false); // Sync temp values when settings change from external sources useEffect(() => { setTempTheme(settings.code_settings.theme); setTempFontSize(settings.code_settings.font_size); - }, [settings.code_settings.theme, settings.code_settings.font_size]); + setTempAutoCollapse(settings.code_settings.auto_collapse_sidebar); + }, [settings.code_settings.theme, settings.code_settings.font_size, settings.code_settings.auto_collapse_sidebar]); const hasChanges = tempTheme !== settings.code_settings.theme || - tempFontSize !== settings.code_settings.font_size; + tempFontSize !== settings.code_settings.font_size || + tempAutoCollapse !== settings.code_settings.auto_collapse_sidebar; const handleSave = async () => { if (!hasChanges) return; @@ -28,7 +32,8 @@ export function CodeSettings() { await updateSettings({ code_settings: { theme: tempTheme, - font_size: tempFontSize + font_size: tempFontSize, + auto_collapse_sidebar: tempAutoCollapse, } }); } catch (error) { @@ -41,6 +46,7 @@ export function CodeSettings() { const handleDiscard = () => { setTempTheme(settings.code_settings.theme); setTempFontSize(settings.code_settings.font_size); + setTempAutoCollapse(settings.code_settings.auto_collapse_sidebar); }; return (
@@ -77,6 +83,23 @@ export function CodeSettings() {
+
+
+
+ +

+ Hide the project sidebar whenever the Code tab is active. It reappears in Chat and History views. +

+
+ +
+
+ {hasChanges && (