Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src-tauri/src/models/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -416,6 +434,11 @@ function AppContent() {
return (
<SidebarWidthProvider>
<SidebarProvider>
<SidebarAutoCollapseManager
activeTab={activeTab}
enabled={Boolean(settings.code_settings?.auto_collapse_sidebar)}
projectActive={Boolean(currentProject)}
/>
<AppSidebar
isSettingsOpen={isSettingsOpen}
setIsSettingsOpen={setIsSettingsOpen}
Expand Down
20 changes: 4 additions & 16 deletions src/components/CodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,10 @@ export function CodeView({ project, tauriInvoke }: CodeViewProps) {
})();
}, [project.path]);

useEffect(() => {
setSelectedFile((prev) => (prev ? null : prev));
}, [viewScope, workspacePath]);

const handleFileSelect = (file: FileInfo) => {
setSelectedFile(file);
};
Expand All @@ -464,22 +468,6 @@ export function CodeView({ project, tauriInvoke }: CodeViewProps) {
{/* File Explorer Sidebar */}
<div className="w-80 border-r bg-muted/30 flex flex-col min-h-0 h-full">
<div className="p-2 border-b bg-muted/20 space-y-2">
<Label className="text-xs text-muted-foreground">View</Label>
{viewScope !== 'workspace' ? (
<Select value={viewScope} onValueChange={(v: 'main' | 'workspace') => { setViewScope(v); setSelectedFile(null); }}>
<SelectTrigger className="h-8 mt-1">
<SelectValue placeholder="Select view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="main">Main Branch</SelectItem>
<SelectItem value="workspace" disabled={!workspacePath}>Workspace</SelectItem>
</SelectContent>
</Select>
) : (
<div className="h-8 mt-1 flex items-center text-xs text-muted-foreground">
Workspace View
</div>
)}
{viewScope !== 'workspace' && (
<Button size="sm" className="w-full" disabled={creatingWs} onClick={() => setIsCreateOpen(true)}>
<Plus className="h-4 w-4 mr-2" /> Create Workspace
Expand Down
16 changes: 8 additions & 8 deletions src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
const [fetchingAgentModels, setFetchingAgentModels] = useState<Record<string, boolean>>({})
const [agentSettingsLoading, setAgentSettingsLoading] = useState(true)
const [agentSettingsError, setAgentSettingsError] = useState<string | null>(null)
const { updateSettings: updateAppSettings } = useAppSettingsContext()
const { updateSettings: updateAppSettings, settings: appSettingsContext } = useAppSettingsContext()

// Code settings
const [codeTheme, setCodeTheme] = useState<string>('github')
Expand Down Expand Up @@ -130,7 +130,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp

// Load app settings with error handling
try {
const appSettings = await invoke<{ show_console_output: boolean, projects_folder: string, file_mentions_enabled: boolean, ui_theme?: string, code_settings?: { theme: string, font_size: number }, chat_send_shortcut?: 'enter' | 'mod+enter', show_welcome_recent_projects?: boolean, max_chat_history?: number, default_cli_agent?: string }>('load_app_settings')
const appSettings = await invoke<{ show_console_output: boolean, projects_folder: string, file_mentions_enabled: boolean, ui_theme?: string, code_settings?: { theme: string, font_size: number, auto_collapse_sidebar?: boolean }, chat_send_shortcut?: 'enter' | 'mod+enter', show_welcome_recent_projects?: boolean, max_chat_history?: number, default_cli_agent?: string }>('load_app_settings')
console.log('✅ App settings loaded:', appSettings)
if (appSettings) {
setShowConsoleOutput(appSettings.show_console_output)
Expand All @@ -156,7 +156,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
setDefaultProjectsFolder(appSettings.projects_folder)
setTempDefaultProjectsFolder(appSettings.projects_folder)
}
const code = appSettings.code_settings || { theme: 'github', font_size: 14 }
const code = appSettings.code_settings || { theme: 'github', font_size: 14, auto_collapse_sidebar: false }
setCodeTheme(code.theme)
setTempCodeTheme(code.theme)
setCodeFontSize(code.font_size)
Expand Down Expand Up @@ -362,7 +362,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
chat_send_shortcut: tempChatSendShortcut,
show_welcome_recent_projects: tempShowWelcomeRecentProjects,
default_cli_agent: tempDefaultCliAgent,
code_settings: { theme: codeTheme, font_size: codeFontSize }
code_settings: { theme: codeTheme, font_size: codeFontSize, auto_collapse_sidebar: appSettingsContext.code_settings.auto_collapse_sidebar }
}
await updateAppSettings(appSettings)
// Also update native window theme
Expand All @@ -388,7 +388,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
chat_send_shortcut: tempChatSendShortcut,
show_welcome_recent_projects: tempShowWelcomeRecentProjects,
default_cli_agent: tempDefaultCliAgent,
code_settings: { theme: codeTheme, font_size: codeFontSize },
code_settings: { theme: codeTheme, font_size: codeFontSize, auto_collapse_sidebar: appSettingsContext.code_settings.auto_collapse_sidebar },
}
await updateAppSettings(appSettings)
setChatSendShortcut(tempChatSendShortcut)
Expand All @@ -411,7 +411,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
chat_send_shortcut: tempChatSendShortcut,
show_welcome_recent_projects: tempShowWelcomeRecentProjects,
default_cli_agent: tempDefaultCliAgent,
code_settings: { theme: codeTheme, font_size: codeFontSize },
code_settings: { theme: codeTheme, font_size: codeFontSize, auto_collapse_sidebar: appSettingsContext.code_settings.auto_collapse_sidebar },
}
await updateAppSettings(appSettings)
setMaxChatHistory(tempMaxChatHistory)
Expand All @@ -435,7 +435,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
chat_send_shortcut: tempChatSendShortcut,
show_welcome_recent_projects: tempShowWelcomeRecentProjects,
default_cli_agent: tempDefaultCliAgent,
code_settings: { theme: codeTheme, font_size: codeFontSize },
code_settings: { theme: codeTheme, font_size: codeFontSize, auto_collapse_sidebar: appSettingsContext.code_settings.auto_collapse_sidebar },
}
await updateAppSettings(appSettings)
setShowWelcomeRecentProjects(tempShowWelcomeRecentProjects)
Expand Down Expand Up @@ -609,7 +609,7 @@ export function SettingsModal({ isOpen, onClose, initialTab }: SettingsModalProp
chat_send_shortcut: tempChatSendShortcut,
show_welcome_recent_projects: tempShowWelcomeRecentProjects,
default_cli_agent: tempDefaultCliAgent,
code_settings: { theme: tempCodeTheme, font_size: tempCodeFontSize }
code_settings: { theme: tempCodeTheme, font_size: tempCodeFontSize, auto_collapse_sidebar: appSettingsContext.code_settings.auto_collapse_sidebar }
}
await updateAppSettings(appSettings)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const defaultSettings = {
file_mentions_enabled: true,
show_welcome_recent_projects: true,
chat_send_shortcut: 'mod+enter',
code_settings: { theme: 'github', font_size: 14 },
code_settings: { theme: 'github', font_size: 14, auto_collapse_sidebar: false },
ui_theme: 'auto',
}

Expand Down
167 changes: 167 additions & 0 deletions src/components/__tests__/App.sidebar.autoCollapse.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import App from '@/App'

const project = {
name: 'Sample Project',
path: '/projects/sample',
last_accessed: Math.floor(Date.now() / 1000),
is_git_repo: true,
git_branch: 'main',
git_status: 'clean',
}

const tauriCore = vi.hoisted(() => ({
invoke: vi.fn(),
}))

vi.mock('@tauri-apps/api/core', () => tauriCore)
vi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn(async () => () => {}) }))
vi.mock('@/components/ChatInterface', () => ({ ChatInterface: () => <div data-testid="chat-interface" /> }))
vi.mock('@/components/CodeView', () => ({ CodeView: () => <div data-testid="code-view" /> }))
vi.mock('@/components/HistoryView', () => ({ HistoryView: () => <div data-testid="history-view" /> }))
vi.mock('@/components/AIAgentStatusBar', () => ({ AIAgentStatusBar: () => <div data-testid="status-bar" /> }))
vi.mock('@/components/ui/tabs', () => {
const React = require('react')
const TabsContext = React.createContext<{ value: string; onValueChange?: (value: string) => void } | null>(null)

const Tabs = ({ value, onValueChange, children }: any) => (
<TabsContext.Provider value={{ value, onValueChange }}>
<div data-testid="tabs" data-active-tab={value}>{children}</div>
</TabsContext.Provider>
)

const TabsList = ({ children, ...props }: any) => (
<div role="tablist" {...props}>{children}</div>
)

const TabsTrigger = ({ value, children, ...props }: any) => {
const context = React.useContext(TabsContext)
if (!context) {
throw new Error('TabsTrigger must be used within Tabs')
}
const isActive = context.value === value
return (
<button
type="button"
role="tab"
data-state={isActive ? 'active' : 'inactive'}
onClick={() => context.onValueChange?.(value)}
{...props}
>
{children}
</button>
)
}

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 (
<div data-state={context.value === value ? 'active' : 'inactive'} {...props}>
{children}
</div>
)
}

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<typeof vi.fn>
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(<App />)

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(<App />)

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'))
})
})
4 changes: 2 additions & 2 deletions src/components/__tests__/App.welcome.recents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand Down
2 changes: 1 addition & 1 deletion src/components/__tests__/CodeView.refresh.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/__tests__/codeview.workspaces.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ if (typeof document !== 'undefined') describe('CodeView workspaces', () => {
<CodeView project={project as any} tauriInvoke={fakeInvoke as any} />
</SettingsProvider>
)
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
Expand Down
2 changes: 1 addition & 1 deletion src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function AppSidebar({ isSettingsOpen, setIsSettingsOpen, onRefreshProject

return (
<ResizableSidebar>
<Sidebar variant="sidebar" className="flex flex-col" {...props}>
<Sidebar variant="sidebar" className="flex flex-col" data-testid="app-sidebar" {...props}>
{/* Sidebar title bar drag area - matching the main content */}
<div
className="h-7 w-full drag-area"
Expand Down
Loading
Loading