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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-07-17 - [Static Analysis Scan Cache]
**Learning:** Workspace static analysis recursively crawls all project files, performing regex and string scanning for warnings (e.g. empty blocks, `console.log`, TODO/FIXME). When typing, React's state is updated and the entire file tree is re-scanned repeatedly on every keystroke, which causes significant performance lag and blocking UI in large workspaces.
**Action:** Use a `WeakMap` to cache computed static analysis results per-item based on the immutable `FileSystemItem` object references. When React does an immutable update, unchanged files retain their reference and bypass re-scanning by using the cache, while only modified files (with new references) are re-evaluated. Old references are automatically garbage collected.
8 changes: 6 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 43 additions & 15 deletions src/components/BottomPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import XTerminal from './XTerminal';
import { useContextMenu } from '../hooks/useContextMenu';
import ContextMenu from './ContextMenu';

interface ProblemItem {
file: string;
path: string;
line: number;
message: string;
severity: 'error' | 'warning' | 'info';
}

export default React.memo(function BottomPanel() {
const { setBottomPanelOpen } = useLayout();
const onClose = useCallback(() => setBottomPanelOpen(false), [setBottomPanelOpen]);
Expand All @@ -32,30 +40,50 @@ export default React.memo(function BottomPanel() {

const { menu, menuRef, showMenu, hideMenu } = useContextMenu();

// Performance Cache: Map each immutable FileSystemItem reference to its static analysis problem details
// Avoids re-scanning unmodified files, dramatically improving responsiveness.
const problemsCache = useMemo(() => new WeakMap<FileSystemItem, ProblemItem[]>(), []);

// Static analysis scanner
const problems = useMemo(() => {
const list: Array<{ file: string; path: string; line: number; message: string; severity: 'error' | 'warning' | 'info' }> = [];
const list: ProblemItem[] = [];
const scan = (items: FileSystemItem[]) => {
for (const item of items) {
if (item.isFolder && item.children) { scan(item.children); continue; }
if (!item.isFolder && item.content) {
item.content.split('\n').forEach((lineText, idx) => {
if (lineText.includes('TODO')) {
list.push({ file: item.name, path: item.path, line: idx + 1, message: lineText.substring(lineText.indexOf('TODO')).replace(/^TODO:?\s*/, '') || 'TODO item', severity: 'info' });
}
if (/\{\s*\}/.test(lineText) && !lineText.includes('=>') && !lineText.includes('const')) {
list.push({ file: item.name, path: item.path, line: idx + 1, message: 'Empty block detected', severity: 'warning' });
}
if (lineText.includes('console.log')) {
list.push({ file: item.name, path: item.path, line: idx + 1, message: 'Remove console.log before production', severity: 'warning' });
}
});
if (item.isFolder && item.children) {
scan(item.children);
continue;
}
if (!item.isFolder) {
// Check WeakMap cache first
const cached = problemsCache.get(item);
if (cached) {
list.push(...cached);
continue;
}

const fileProblems: ProblemItem[] = [];
if (item.content) {
item.content.split('\n').forEach((lineText, idx) => {
if (lineText.includes('TODO')) {
fileProblems.push({ file: item.name, path: item.path, line: idx + 1, message: lineText.substring(lineText.indexOf('TODO')).replace(/^TODO:?\s*/, '') || 'TODO item', severity: 'info' });
}
if (/\{\s*\}/.test(lineText) && !lineText.includes('=>') && !lineText.includes('const')) {
fileProblems.push({ file: item.name, path: item.path, line: idx + 1, message: 'Empty block detected', severity: 'warning' });
}
if (lineText.includes('console.log')) {
fileProblems.push({ file: item.name, path: item.path, line: idx + 1, message: 'Remove console.log before production', severity: 'warning' });
}
});
}

problemsCache.set(item, fileProblems);
list.push(...fileProblems);
}
}
};
scan(files);
return list;
}, [files]);
}, [files, problemsCache]);

useEffect(() => {
if (window.electronAPI) {
Expand Down
50 changes: 35 additions & 15 deletions src/contexts/FileSystemContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export function FileSystemProvider({ children }: { children: ReactNode }) {
const [clipboard, setClipboard] = useState<ClipboardData | null>(null);
const [undoHistory, setUndoHistory] = useState<UndoStack>({});

// Performance Cache: Map each immutable FileSystemItem reference to its static analysis problem count
// Avoids re-computing empty block, console.log, TODO, and FIXME warnings on unmodified files
const problemsCountCache = useMemo(() => new WeakMap<FileSystemItem, { errors: number; warnings: number }>(), []);

const pushHistory = useCallback((path: string, oldContent: string) => {
if (!path || path === 'welcome' || path.startsWith('docs/')) return;
if (!oldContent) return;
Expand Down Expand Up @@ -202,29 +206,45 @@ export function FileSystemProvider({ children }: { children: ReactNode }) {
for (const item of items) {
if (item.isFolder && item.children) {
scan(item.children);
} else if (!item.isFolder && item.content) {
const lines = item.content.split('\n');
for (const lineText of lines) {
if (/\{\s*\}/.test(lineText) && !lineText.includes('=>') && !lineText.includes('const')) {
warnings++;
}
if (lineText.includes('console.log')) {
warnings++;
}
if (lineText.includes('TODO')) {
warnings++;
}
if (lineText.includes('FIXME')) {
warnings++;
} else if (!item.isFolder) {
// Check WeakMap cache first
const cached = problemsCountCache.get(item);
if (cached) {
errors += cached.errors;
warnings += cached.warnings;
continue;
}

let fileErrors = 0;
let fileWarnings = 0;
if (item.content) {
const lines = item.content.split('\n');
for (const lineText of lines) {
if (/\{\s*\}/.test(lineText) && !lineText.includes('=>') && !lineText.includes('const')) {
fileWarnings++;
}
if (lineText.includes('console.log')) {
fileWarnings++;
}
if (lineText.includes('TODO')) {
fileWarnings++;
}
if (lineText.includes('FIXME')) {
fileWarnings++;
}
}
}

problemsCountCache.set(item, { errors: fileErrors, warnings: fileWarnings });
errors += fileErrors;
warnings += fileWarnings;
}
}
};

scan(files);
return { errors, warnings };
}, [files]);
}, [files, problemsCountCache]);

const handleFileSelect = useCallback(async (path: string) => {
if (path === 'welcome') {
Expand Down
Loading