From e57f17996f491dc68a218ea297443f56b0bdc482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ng=C3=B4=20Th=C6=B0=E1=BB=A3ng?= <83950837+nguyenngothuong@users.noreply.github.com> Date: Sat, 14 Mar 2026 05:46:43 +0700 Subject: [PATCH] feat: auto-save file editor with 1.5s debounce (#649) * fix: project label text hidden by selection overlay in dark mode In NavRail's ProjectTile, the expanded active item renders an absolutely positioned background overlay (--interactive-selection) that visually covers the text label. The label span was missing 'relative z-10', unlike NavRailActionButton which correctly uses it. Fixes text being invisible on selected project in dark themes (nord-dark, dracula-dark, monokai-dark, gruvbox-dark, etc.) * feat: auto-save file editor with 1.5s debounce Add debounced auto-save to the file editor so users no longer need to manually save with Cmd/Ctrl+S or the save button. Files are automatically saved 1.5 seconds after the user stops typing. - Debounced auto-save effect that triggers on draft content changes - Cmd/Ctrl+S still works for immediate save (cancels pending debounce) - Updated save button UI with contextual status indicators: 'Saving...' spinner, 'Saved' checkmark, or save icon when dirty - Reset auto-save status when switching between files - Applied consistently to both normal and fullscreen editor views --- .../ui/src/components/views/FilesView.tsx | 113 +++++++++++++----- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 826623ac..f34b3e60 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -549,6 +549,8 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [draftContent, setDraftContent] = React.useState(''); const [isSaving, setIsSaving] = React.useState(false); + const autoSaveTimerRef = React.useRef | null>(null); + const [autoSaveStatus, setAutoSaveStatus] = React.useState<'idle' | 'saved'>('idle'); const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false); const pendingSelectFileRef = React.useRef(null); @@ -1150,6 +1152,35 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }; }, [isDirty, setMainTabGuard]); + // Auto-save: debounce 1.5s after user stops typing + const AUTO_SAVE_DELAY = 1500; + + React.useEffect(() => { + const canWrite = Boolean(selectedFile && files.writeFile); + if (!isDirty || !canWrite || isSaving) { + return; + } + + autoSaveTimerRef.current = setTimeout(() => { + void saveDraft().then(() => { + setAutoSaveStatus('saved'); + setTimeout(() => setAutoSaveStatus('idle'), 2000); + }); + }, AUTO_SAVE_DELAY); + + return () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + autoSaveTimerRef.current = null; + } + }; + }, [draftContent, isDirty, selectedFile, files.writeFile, isSaving, saveDraft]); + + // Reset auto-save status when switching files + React.useEffect(() => { + setAutoSaveStatus('idle'); + }, [selectedFile?.path]); + React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!hasModifier(e)) { @@ -1158,8 +1189,16 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { if (e.key.toLowerCase() === 's') { e.preventDefault(); + // Cancel pending auto-save; user wants immediate save + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + autoSaveTimerRef.current = null; + } if (!isSaving) { - void saveDraft(); + void saveDraft().then(() => { + setAutoSaveStatus('saved'); + setTimeout(() => setAutoSaveStatus('idle'), 2000); + }); } } else if (e.key.toLowerCase() === 'f') { e.preventDefault(); @@ -2253,21 +2292,28 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { {selectedFile && (
{canEdit && textViewMode === 'edit' && ( - + isSaving ? ( + + + Saving… + + ) : autoSaveStatus === 'saved' && !isDirty ? ( + + + Saved + + ) : isDirty ? ( + + ) : null )} @@ -2714,21 +2760,28 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
{canEdit && textViewMode === 'edit' && ( - + + ) : null )}