import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useAppStore } from '../../store'; import type { GitInitProgress, GitHistoryEntry } from '../../../main/shared/electronApi'; import './GitSidebar.css'; import '../Sidebar/Sidebar.css'; export const GitSidebar: React.FC = () => { const { activeProject, openTab, tabs, closeTab } = useAppStore(); const [projectPath, setProjectPath] = useState(null); const [loading, setLoading] = useState(true); const [initializing, setInitializing] = useState(false); const [statusLoading, setStatusLoading] = useState(false); const [actionLoading, setActionLoading] = useState<'fetch' | 'pull' | 'push' | 'commit' | null>(null); const [error, setError] = useState(null); const [errorGuidance, setErrorGuidance] = useState([]); const [isRepo, setIsRepo] = useState(false); const [currentBranch, setCurrentBranch] = useState(null); const [statusFiles, setStatusFiles] = useState>([]); const [commitMessage, setCommitMessage] = useState(''); const [historyLoading, setHistoryLoading] = useState(false); const [historyEntries, setHistoryEntries] = useState([]); const [initProgress, setInitProgress] = useState(null); const [initTranscript, setInitTranscript] = useState([]); const [isTranscriptExpanded, setIsTranscriptExpanded] = useState(false); const remoteUrlInputRef = useRef(null); const commitMessageInputRef = useRef(null); const getDiffTabId = (filePath: string): string => `git-diff:${filePath}`; const getCommitDiffTabId = (commitHash: string): string => `git-diff:commit:${commitHash}`; const getActionProgressMessage = (action: 'fetch' | 'pull' | 'push' | 'commit'): string => { if (action === 'push') { return 'Pushing commits to remote... this can take a while for large uploads.'; } if (action === 'fetch') { return 'Fetching remote updates...'; } if (action === 'pull') { return 'Pulling latest changes...'; } return 'Creating commit...'; }; const openDiffTab = useCallback( (filePath: string, isTransient: boolean) => { openTab({ type: 'git-diff', id: getDiffTabId(filePath), isTransient, }); }, [openTab], ); const openCommitDiffTab = useCallback( (commitHash: string, isTransient: boolean) => { openTab({ type: 'git-diff', id: getCommitDiffTabId(commitHash), isTransient, }); }, [openTab], ); const resolveProjectPath = useCallback(async (): Promise => { if (!activeProject) { return null; } if (activeProject.dataPath) { return activeProject.dataPath; } return window.electronAPI.app.getDefaultProjectPath(activeProject.id); }, [activeProject]); const loadRepoState = useCallback(async () => { setLoading(true); setError(null); setErrorGuidance([]); try { const availability = await window.electronAPI.git.checkAvailability(); if (!availability.gitFound) { setError('Git executable not found. Please install Git and restart the app.'); setIsRepo(false); return; } const resolvedProjectPath = await resolveProjectPath(); setProjectPath(resolvedProjectPath); if (!resolvedProjectPath) { setError('No active project selected.'); setIsRepo(false); return; } const repoState = await window.electronAPI.git.getRepoState(resolvedProjectPath); setIsRepo(repoState.isRepo); setCurrentBranch(repoState.currentBranch || null); if (repoState.isRepo) { setStatusLoading(true); setHistoryLoading(true); try { const [status, history] = await Promise.all([ window.electronAPI.git.getStatus(resolvedProjectPath), window.electronAPI.git.getHistory(resolvedProjectPath, 20), ]); setStatusFiles(status.files); setHistoryEntries(history); } finally { setStatusLoading(false); setHistoryLoading(false); } } else { setStatusFiles([]); setHistoryEntries([]); } } catch { setError('Unable to load repository status.'); setIsRepo(false); setStatusFiles([]); setHistoryEntries([]); } finally { setLoading(false); } }, [resolveProjectPath]); useEffect(() => { void loadRepoState(); }, [loadRepoState]); useEffect(() => { const unsubscribe = window.electronAPI.git.onInitProgress((progress) => { setInitProgress(progress); setInitTranscript((previous) => [...previous, progress].slice(-12)); if (progress.phase === 'failed') { setIsTranscriptExpanded(true); } }); return () => { unsubscribe(); }; }, []); const handleInitialize = async () => { if (!projectPath) { return; } setInitializing(true); setError(null); setInitTranscript([]); setInitProgress({ phase: 'initializing-repo', progress: 0, message: 'Preparing repository initialization...', }); try { const normalizedRemoteUrl = remoteUrlInputRef.current?.value.trim() || ''; const result = normalizedRemoteUrl ? await window.electronAPI.git.init(projectPath, normalizedRemoteUrl) : await window.electronAPI.git.init(projectPath); if (!result.success) { setError(result.error || 'Failed to initialize git repository.'); return; } await loadRepoState(); } catch { setError('Failed to initialize git repository.'); } finally { setInitializing(false); } }; const handleRepoAction = async (action: 'fetch' | 'pull' | 'push') => { if (actionLoading) { return; } const effectiveProjectPath = projectPath ?? (await resolveProjectPath()); if (!effectiveProjectPath) { setError('No active project selected.'); return; } if (!projectPath) { setProjectPath(effectiveProjectPath); } setActionLoading(action); setError(null); setErrorGuidance([]); try { const result = action === 'fetch' ? await window.electronAPI.git.fetch(effectiveProjectPath) : action === 'pull' ? await window.electronAPI.git.pull(effectiveProjectPath) : await window.electronAPI.git.push(effectiveProjectPath); if (!result.success) { setError(result.error || `Failed to ${action}.`); setErrorGuidance(result.guidance || []); return; } await loadRepoState(); } catch { setError(`Failed to ${action}.`); } finally { setActionLoading(null); } }; const handleCommit = async () => { if (actionLoading) { return; } const effectiveProjectPath = projectPath ?? (await resolveProjectPath()); if (!effectiveProjectPath) { setError('No active project selected.'); return; } if (!projectPath) { setProjectPath(effectiveProjectPath); } setActionLoading('commit'); setError(null); setErrorGuidance([]); try { const messageToCommit = commitMessageInputRef.current?.value ?? commitMessage; const result = await window.electronAPI.git.commitAll(effectiveProjectPath, messageToCommit); if (!result.success) { setError(result.error || 'Failed to commit changes.'); setErrorGuidance(result.guidance || []); return; } tabs .filter((tab) => tab.type === 'git-diff') .forEach((tab) => closeTab(tab.id)); setCommitMessage(''); await loadRepoState(); } catch { setError('Failed to commit changes.'); } finally { setActionLoading(null); } }; if (loading) { return (
SOURCE CONTROL
Loading...
); } const transcriptSection = initTranscript.length > 0 ? (
{isTranscriptExpanded && (
    {initTranscript.map((entry, index) => (
  • {entry.progress}% — {entry.message.toLowerCase().replace(/\.+$/, '')} {entry.detail ? ` (${entry.detail})` : ''}
  • ))}
)}
) : null; if (isRepo) { return (
SOURCE CONTROL
{actionLoading && (
{getActionProgressMessage(actionLoading)}
)}
Open Changes ({statusFiles.length})
setCommitMessage(event.target.value)} disabled={actionLoading !== null} />
{statusLoading ? (
Loading changes...
) : statusFiles.length === 0 ? (
No changes
) : (
{statusFiles.map((file) => ( ))}
)}
Version History ({historyEntries.length})
{historyLoading ? (
Loading history...
) : historyEntries.length === 0 ? (
No commits yet
) : (
{historyEntries.map((entry) => ( ))}
)} {currentBranch &&
Branch: {currentBranch}
}
{error && (
{error}
{errorGuidance.length > 0 && (
    {errorGuidance.map((step) => (
  • {step}
  • ))}
)}
)} {transcriptSection}
); } return (
SOURCE CONTROL

This project is not a git repository.

{initializing && (

{initProgress?.message || 'Initializing repository...'} {typeof initProgress?.progress === 'number' ? ` (${initProgress.progress}%)` : ''} {initProgress?.detail ? ` — ${initProgress.detail}` : ''}

)} {error &&

{error}

}
{transcriptSection}
); };