diff --git a/server/routes/git.js b/server/routes/git.js index da917d0..0f4f10d 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => { */ async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) { // Create the prompt - const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format. + const prompt = `Generate a conventional commit message for these changes. REQUIREMENTS: -- Use conventional commit format: type(scope): subject -- Include a body that explains what changed and why -- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore -- Keep subject line under 50 characters -- Wrap body at 72 characters -- Be specific and descriptive -- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks +- Format: type(scope): subject +- Include body explaining what changed and why +- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore +- Subject under 50 chars, body wrapped at 72 chars +- Focus on user-facing changes, not implementation details +- Consider what's being added AND removed +- Return ONLY the commit message (no markdown, explanations, or code blocks) FILES CHANGED: ${files.map(f => `- ${f}`).join('\n')} @@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')} DIFFS: ${diffContext.substring(0, 4000)} -Generate the commit message now:`; +Generate the commit message:`; try { // Create a simple writer that collects the response diff --git a/src/App.jsx b/src/App.jsx index 3a84f0f..4d10c9c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -55,6 +55,7 @@ function AppContent() { const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isInputFocused, setIsInputFocused] = useState(false); const [showSettings, setShowSettings] = useState(false); + const [settingsInitialTab, setSettingsInitialTab] = useState('tools'); const [showQuickSettings, setShowQuickSettings] = useState(false); const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false); const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false); @@ -308,6 +309,12 @@ function AppContent() { // Expose fetchProjects globally for component access window.refreshProjects = fetchProjects; + // Expose openSettings function globally for component access + window.openSettings = useCallback((tab = 'tools') => { + setSettingsInitialTab(tab); + setShowSettings(true); + }, []); + // Handle URL-based session loading useEffect(() => { if (sessionId && projects.length > 0) { @@ -927,6 +934,7 @@ function AppContent() { isOpen={showSettings} onClose={() => setShowSettings(false)} projects={projects} + initialTab={settingsInitialTab} /> {/* Version Upgrade Modal */} diff --git a/src/components/CodeEditor.jsx b/src/components/CodeEditor.jsx index 770a1c8..172242d 100644 --- a/src/components/CodeEditor.jsx +++ b/src/components/CodeEditor.jsx @@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark'; import { EditorView, showPanel, ViewPlugin } from '@codemirror/view'; import { unifiedMergeView, getChunks } from '@codemirror/merge'; import { showMinimap } from '@replit/codemirror-minimap'; -import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react'; +import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react'; import { api } from '../utils/api'; -function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { +function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) { const [content, setContent] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(true); + const [isDarkMode, setIsDarkMode] = useState(() => { + const savedTheme = localStorage.getItem('codeEditorTheme'); + return savedTheme ? savedTheme === 'dark' : true; + }); const [saveSuccess, setSaveSuccess] = useState(false); const [showDiff, setShowDiff] = useState(!!file.diffInfo); - const [wordWrap, setWordWrap] = useState(false); + const [wordWrap, setWordWrap] = useState(() => { + return localStorage.getItem('codeEditorWordWrap') === 'true'; + }); + const [minimapEnabled, setMinimapEnabled] = useState(() => { + return localStorage.getItem('codeEditorShowMinimap') !== 'false'; + }); + const [showLineNumbers, setShowLineNumbers] = useState(() => { + return localStorage.getItem('codeEditorLineNumbers') !== 'false'; + }); + const [fontSize, setFontSize] = useState(() => { + return localStorage.getItem('codeEditorFontSize') || '14'; + }); const editorRef = useRef(null); // Create minimap extension with chunk-based gutters const minimapExtension = useMemo(() => { - if (!file.diffInfo || !showDiff) return []; + if (!file.diffInfo || !showDiff || !minimapEnabled) return []; const gutters = {}; @@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { }; }) ]; - }, [file.diffInfo, showDiff, isDarkMode]); + }, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]); // Create extension to scroll to first chunk on mount const scrollToFirstChunkExtension = useMemo(() => { @@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { ]; }, [file.diffInfo, showDiff]); - // Create diff navigation panel extension - const diffNavigationPanel = useMemo(() => { - if (!file.diffInfo || !showDiff) return []; - + // Create editor toolbar panel - always visible + const editorToolbarPanel = useMemo(() => { const createPanel = (view) => { const dom = document.createElement('div'); - dom.className = 'cm-diff-navigation-panel'; + dom.className = 'cm-editor-toolbar-panel'; let currentIndex = 0; const updatePanel = () => { - // Use getChunks API to get ALL chunks regardless of viewport - const chunksData = getChunks(view.state); + // Check if we have diff info and it's enabled + const hasDiff = file.diffInfo && showDiff; + const chunksData = hasDiff ? getChunks(view.state) : null; const chunks = chunksData?.chunks || []; const chunkCount = chunks.length; - dom.innerHTML = ` -
+ // Build the toolbar HTML + let toolbarHTML = '
'; + + // Left side - diff navigation (if applicable) + toolbarHTML += '
'; + if (hasDiff) { + toolbarHTML += ` ${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes -
+ `; + } + toolbarHTML += '
'; + + // Right side - action buttons + toolbarHTML += '
'; + + // Show/hide diff button (only if there's diff info) + if (file.diffInfo) { + toolbarHTML += ` + + `; + } + + // Settings button + toolbarHTML += ` + `; - const prevBtn = dom.querySelector('.cm-diff-nav-prev'); - const nextBtn = dom.querySelector('.cm-diff-nav-next'); + // Expand button (only in sidebar mode) + if (isSidebar && onToggleExpand) { + toolbarHTML += ` + + `; + } - prevBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; + toolbarHTML += '
'; + toolbarHTML += '
'; - // Navigate to the chunk - use fromB which is the position in the current document - const chunk = chunks[currentIndex]; - if (chunk) { - // Scroll to the start of the chunk in the B side (current document) - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); + dom.innerHTML = toolbarHTML; + + // Attach event listeners for diff navigation + if (hasDiff) { + const prevBtn = dom.querySelector('.cm-diff-nav-prev'); + const nextBtn = dom.querySelector('.cm-diff-nav-next'); + + prevBtn?.addEventListener('click', () => { + if (chunks.length === 0) return; + currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; + + const chunk = chunks[currentIndex]; + if (chunk) { + view.dispatch({ + effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) + }); + } + updatePanel(); + }); + + nextBtn?.addEventListener('click', () => { + if (chunks.length === 0) return; + currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; + + const chunk = chunks[currentIndex]; + if (chunk) { + view.dispatch({ + effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) + }); + } + updatePanel(); + }); + } + + // Attach event listener for toggle diff button + if (file.diffInfo) { + const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn'); + toggleDiffBtn?.addEventListener('click', () => { + setShowDiff(!showDiff); + }); + } + + // Attach event listener for settings button + const settingsBtn = dom.querySelector('.cm-settings-btn'); + settingsBtn?.addEventListener('click', () => { + if (window.openSettings) { + window.openSettings('appearance'); } - updatePanel(); }); - nextBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; - - // Navigate to the chunk - use fromB which is the position in the current document - const chunk = chunks[currentIndex]; - if (chunk) { - // Scroll to the start of the chunk in the B side (current document) - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); - } - updatePanel(); - }); + // Attach event listener for expand button + if (isSidebar && onToggleExpand) { + const expandBtn = dom.querySelector('.cm-expand-btn'); + expandBtn?.addEventListener('click', () => { + onToggleExpand(); + }); + } }; updatePanel(); @@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { }; return [showPanel.of(createPanel)]; - }, [file.diffInfo, showDiff]); + }, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]); // Get language extension based on file extension const getLanguageExtension = (filename) => { @@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { setIsFullscreen(!isFullscreen); }; + // Save theme preference to localStorage + useEffect(() => { + localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light'); + }, [isDarkMode]); + + // Save word wrap preference to localStorage + useEffect(() => { + localStorage.setItem('codeEditorWordWrap', wordWrap.toString()); + }, [wordWrap]); + + // Listen for settings changes from the Settings modal + useEffect(() => { + const handleStorageChange = () => { + const newTheme = localStorage.getItem('codeEditorTheme'); + if (newTheme) { + setIsDarkMode(newTheme === 'dark'); + } + + const newWordWrap = localStorage.getItem('codeEditorWordWrap'); + if (newWordWrap !== null) { + setWordWrap(newWordWrap === 'true'); + } + + const newShowMinimap = localStorage.getItem('codeEditorShowMinimap'); + if (newShowMinimap !== null) { + setMinimapEnabled(newShowMinimap !== 'false'); + } + + const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers'); + if (newShowLineNumbers !== null) { + setShowLineNumbers(newShowLineNumbers !== 'false'); + } + + const newFontSize = localStorage.getItem('codeEditorFontSize'); + if (newFontSize) { + setFontSize(newFontSize); + } + }; + + // Listen for storage events (changes from other tabs/windows) + window.addEventListener('storage', handleStorageChange); + + // Custom event for same-window updates + window.addEventListener('codeEditorSettingsChanged', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('codeEditorSettingsChanged', handleStorageChange); + }; + }, []); + // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (e) => { @@ -329,7 +467,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { ) : ( -
+
@@ -381,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'}; } - /* Diff navigation panel styling */ - .cm-diff-navigation-panel { + /* Editor toolbar panel styling */ + .cm-editor-toolbar-panel { padding: 8px 12px; background-color: ${isDarkMode ? '#1f2937' : '#ffffff'}; border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'}; @@ -390,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { font-size: 14px; } - .cm-diff-nav-btn { + .cm-diff-nav-btn, + .cm-toolbar-btn { padding: 4px; background: transparent; border: none; @@ -400,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { align-items: center; justify-content: center; color: inherit; + transition: background-color 0.2s; } - .cm-diff-nav-btn:hover { + .cm-diff-nav-btn:hover, + .cm-toolbar-btn:hover { background-color: ${isDarkMode ? '#374151' : '#f3f4f6'}; } @@ -414,7 +555,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
@@ -433,7 +574,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {

{file.name}

{file.diffInfo && ( - 📝 Has changes + Showing changes )}
@@ -442,36 +583,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
- {file.diffInfo && ( - - )} - - - - - +
+
+ + {/* Word Wrap */} +
+
+
+
+ Word Wrap +
+
+ Enable word wrapping by default in the editor +
+
+ +
+
+ + {/* Show Minimap */} +
+
+
+
+ Show Minimap +
+
+ Display a minimap for easier navigation in diff view +
+
+ +
+
+ + {/* Show Line Numbers */} +
+
+
+
+ Show Line Numbers +
+
+ Display line numbers in the editor +
+
+ +
+
+ + {/* Font Size */} +
+
+
+
+ Font Size +
+
+ Editor font size in pixels +
+
+ +
+
+
)} @@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) { type="checkbox" checked={skipPermissions} onChange={(e) => setSkipPermissions(e.target.checked)} - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" + className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600" />
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) { type="checkbox" checked={cursorSkipPermissions} onChange={(e) => setCursorSkipPermissions(e.target.checked)} - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" + className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600" />