/** * GeneralSection - General settings including startup, appearance, browser access, and local Claude root. */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { api, isElectronMode } from '@renderer/api'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { Combobox } from '@renderer/components/ui/combobox'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { getFullResetState } from '@renderer/store/utils/stateResetHelpers'; import { AGENT_LANGUAGE_OPTIONS, resolveLanguageName } from '@shared/utils/agentLanguage'; import { Check, Copy, FolderOpen, Laptop, Loader2, RotateCcw } from 'lucide-react'; import { SettingRow, SettingsSectionHeader, SettingsToggle } from '../components'; import type { SafeConfig } from '../hooks/useSettingsConfig'; import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types'; import type { HttpServerStatus } from '@shared/types/api'; import type { AppConfig } from '@shared/types/notifications'; // Theme options const THEME_OPTIONS = [ { value: 'dark', label: 'Dark' }, { value: 'light', label: 'Light' }, { value: 'system', label: 'System' }, ] as const; interface GeneralSectionProps { readonly safeConfig: SafeConfig; readonly saving: boolean; readonly onGeneralToggle: (key: keyof AppConfig['general'], value: boolean) => void; readonly onThemeChange: (value: 'dark' | 'light' | 'system') => void; readonly onLanguageChange: (value: string) => void; } export const GeneralSection = ({ safeConfig, saving, onGeneralToggle, onThemeChange, onLanguageChange, }: GeneralSectionProps): React.JSX.Element => { const [serverStatus, setServerStatus] = useState({ running: false, port: 3456, }); const [serverLoading, setServerLoading] = useState(false); const [copied, setCopied] = useState(false); // Claude Root state const connectionMode = useStore((s) => s.connectionMode); const fetchProjects = useStore((s) => s.fetchProjects); const fetchRepositoryGroups = useStore((s) => s.fetchRepositoryGroups); const [claudeRootInfo, setClaudeRootInfo] = useState(null); const [updatingClaudeRoot, setUpdatingClaudeRoot] = useState(false); const [claudeRootError, setClaudeRootError] = useState(null); const [findingWslRoots, setFindingWslRoots] = useState(false); const [wslCandidates, setWslCandidates] = useState([]); const [showWslModal, setShowWslModal] = useState(false); // Fetch server status and Claude root info on mount useEffect(() => { void api.httpServer.getStatus().then(setServerStatus); }, []); const loadClaudeRootInfo = useCallback(async () => { try { const info = await api.config.getClaudeRootInfo(); setClaudeRootInfo(info); } catch (error) { setClaudeRootError( error instanceof Error ? error.message : 'Failed to load local Claude root settings' ); } }, []); useEffect(() => { void loadClaudeRootInfo(); }, [loadClaudeRootInfo]); const handleServerToggle = useCallback(async (enabled: boolean) => { setServerLoading(true); try { const status = enabled ? await api.httpServer.start() : await api.httpServer.stop(); setServerStatus(status); } catch { // Status didn't change } finally { setServerLoading(false); } }, []); const serverUrl = `http://localhost:${serverStatus.port}`; const handleCopyUrl = useCallback(() => { void navigator.clipboard.writeText(serverUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [serverUrl]); // Claude Root handlers const resetWorkspaceForRootChange = useCallback((): void => { useStore.setState({ projects: [], repositoryGroups: [], openTabs: [], activeTabId: null, selectedTabIds: [], paneLayout: { panes: [ { id: 'pane-default', tabs: [], activeTabId: null, selectedTabIds: [], widthFraction: 1, }, ], focusedPaneId: 'pane-default', }, ...getFullResetState(), }); }, []); const applyClaudeRootPath = useCallback( async (claudeRootPath: string | null): Promise => { try { setUpdatingClaudeRoot(true); setClaudeRootError(null); await api.config.update('general', { claudeRootPath }); await loadClaudeRootInfo(); if (connectionMode === 'local') { resetWorkspaceForRootChange(); await Promise.all([fetchProjects(), fetchRepositoryGroups()]); } } catch (error) { setClaudeRootError(error instanceof Error ? error.message : 'Failed to update Claude root'); } finally { setUpdatingClaudeRoot(false); } }, [ connectionMode, fetchProjects, fetchRepositoryGroups, loadClaudeRootInfo, resetWorkspaceForRootChange, ] ); const handleSelectClaudeRootFolder = useCallback(async (): Promise => { setClaudeRootError(null); const selection = await api.config.selectClaudeRootFolder(); if (!selection) { return; } if (!selection.isClaudeDirName) { const proceed = await confirm({ title: 'Selected folder is not .claude', message: `This folder is named "${selection.path.split(/[\\/]/).pop() ?? selection.path}", not ".claude". Continue anyway?`, confirmLabel: 'Use Folder', }); if (!proceed) { return; } } if (!selection.hasProjectsDir) { const proceed = await confirm({ title: 'No projects directory found', message: 'This folder does not contain a "projects" directory. Continue anyway?', confirmLabel: 'Use Folder', }); if (!proceed) { return; } } await applyClaudeRootPath(selection.path); }, [applyClaudeRootPath]); const handleResetClaudeRoot = useCallback(async (): Promise => { await applyClaudeRootPath(null); }, [applyClaudeRootPath]); const applyWslCandidate = useCallback( async (candidate: WslClaudeRootCandidate): Promise => { if (!candidate.hasProjectsDir) { const proceed = await confirm({ title: 'WSL path missing projects directory', message: `"${candidate.path}" does not contain a "projects" directory. Continue anyway?`, confirmLabel: 'Use Path', }); if (!proceed) { return; } } await applyClaudeRootPath(candidate.path); setShowWslModal(false); }, [applyClaudeRootPath] ); const handleUseWslForClaude = useCallback(async (): Promise => { try { setFindingWslRoots(true); setClaudeRootError(null); const candidates = await api.config.findWslClaudeRoots(); setWslCandidates(candidates); if (candidates.length === 0) { const pickManually = await confirm({ title: 'No WSL Claude paths found', message: 'Could not find WSL distros with Claude data automatically. Select folder manually?', confirmLabel: 'Select Folder', }); if (pickManually) { await handleSelectClaudeRootFolder(); } return; } const candidatesWithProjects = candidates.filter((candidate) => candidate.hasProjectsDir); if (candidatesWithProjects.length === 1) { await applyWslCandidate(candidatesWithProjects[0]); return; } setShowWslModal(true); } catch (error) { setClaudeRootError( error instanceof Error ? error.message : 'Failed to detect WSL Claude root paths' ); } finally { setFindingWslRoots(false); } }, [applyWslCandidate, handleSelectClaudeRootFolder]); const isCustomClaudeRoot = Boolean(claudeRootInfo?.customPath); const resolvedClaudeRootPath = claudeRootInfo?.resolvedPath ?? '~/.claude'; const defaultClaudeRootPath = claudeRootInfo?.defaultPath ?? '~/.claude'; const isWindowsStyleDefaultPath = /^[a-zA-Z]:\\/.test(defaultClaudeRootPath) || defaultClaudeRootPath.startsWith('\\\\'); const isElectron = useMemo(() => isElectronMode(), []); const agentLanguageDescription = useMemo(() => { const current = safeConfig.general.agentLanguage ?? 'system'; if (current === 'system') { const browserLang = navigator.language; const primaryCode = browserLang.includes('-') ? browserLang.split('-')[0] : browserLang; const detected = resolveLanguageName('system', browserLang); const detectedFlag = AGENT_LANGUAGE_OPTIONS.find((o) => o.value === primaryCode)?.flag ?? ''; const flagPrefix = detectedFlag ? `${detectedFlag} ` : ''; return `Language for agent communication (detected: ${flagPrefix}${detected})`; } return 'Language for agent communication'; }, [safeConfig.general.agentLanguage]); const languageComboboxOptions = useMemo( () => AGENT_LANGUAGE_OPTIONS.map((opt) => ({ value: opt.value, label: `${opt.flag} ${opt.label}`, meta: { flag: opt.flag }, })), [] ); const renderLanguageOption = useCallback( ( option: { value: string; label: string; meta?: Record }, isSelected: boolean ) => ( <> {option.label} ), [] ); return (
{isElectron && ( <> onGeneralToggle('launchAtLogin', v)} disabled={saving} /> {window.navigator.userAgent.includes('Macintosh') && ( onGeneralToggle('showDockIcon', v)} disabled={saving} /> )} )}
{THEME_OPTIONS.map((opt) => ( ))}
onGeneralToggle('autoExpandAIGroups', v)} disabled={saving} /> {isElectron && !window.navigator.userAgent.includes('Macintosh') && ( { const shouldRelaunch = await confirm({ title: 'Restart required', message: 'The app needs to restart to apply the title bar change. Restart now?', confirmLabel: 'Restart', }); if (shouldRelaunch) { // Await config write before relaunch to avoid race condition on Windows // (antivirus/NTFS can delay file writes beyond a fixed timeout) try { await api.config.update('general', { useNativeTitleBar: v }); } catch { // If save fails, still try to toggle via the normal path onGeneralToggle('useNativeTitleBar', v); await new Promise((r) => setTimeout(r, 500)); } void window.electronAPI?.windowControls?.relaunch(); } }} disabled={saving} /> )} {isElectron && ( <>

Choose which local folder is treated as your Claude data root

{resolvedClaudeRootPath}
Auto-detected: {defaultClaudeRootPath}
{isWindowsStyleDefaultPath && ( )}
{claudeRootError && (

{claudeRootError}

)} {showWslModal && (
))}
)} )} {isElectron ? ( <> {serverLoading ? ( ) : ( )} {serverStatus.running && (
Running on {serverUrl}
)} ) : ( <>
Running on {window.location.origin}

Running in standalone mode. The HTTP server is always active. System notifications are not available — notification triggers are logged in-app only.

)} {/* Privacy / Telemetry — only visible when Sentry DSN is baked into the build */} {import.meta.env.VITE_SENTRY_DSN && ( <> onGeneralToggle('telemetryEnabled', v)} disabled={saving} /> )}
); };