mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Fix 75 ESLint errors by updating eslint.config.mjs: - Add missing browser globals (MouseEvent, AbortController, Response, etc.) - Add Vite define global (__APP_VERSION__) - Configure @ts-nocheck to require descriptions - Add no-unused-vars rule for .mjs scripts - Fix runtime bug in agent-output-modal.tsx (setOutput -> setStreamedContent) - Remove ~120 unused variable warnings across 97 files: - Remove unused imports (React hooks, lucide icons, types) - Remove unused constants and variables - Remove unused function definitions - Prefix intentionally unused parameters with underscore - Add descriptions to all @ts-nocheck comments (25 files) - Clean up misc issues: - Remove invalid deprecation plugin comments - Fix eslint-disable comment placement - Add missing RefreshCw import in code-view.tsx Reduces lint warnings from ~300 to 67 (all remaining are no-explicit-any) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
266 lines
8.4 KiB
TypeScript
266 lines
8.4 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react';
|
|
import { createLogger } from '@automaker/utils/logger';
|
|
import { useAppStore } from '@/store/app-store';
|
|
import { getElectronAPI } from '@/lib/electron';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { File, Folder, FolderOpen, ChevronRight, ChevronDown, Code, RefreshCw } from 'lucide-react';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const logger = createLogger('CodeView');
|
|
|
|
interface FileTreeNode {
|
|
name: string;
|
|
path: string;
|
|
isDirectory: boolean;
|
|
children?: FileTreeNode[];
|
|
isExpanded?: boolean;
|
|
}
|
|
|
|
const IGNORE_PATTERNS = ['node_modules', '.git', '.next', 'dist', 'build', '.DS_Store', '*.log'];
|
|
|
|
const shouldIgnore = (name: string) => {
|
|
return IGNORE_PATTERNS.some((pattern) => {
|
|
if (pattern.startsWith('*')) {
|
|
return name.endsWith(pattern.slice(1));
|
|
}
|
|
return name === pattern;
|
|
});
|
|
};
|
|
|
|
export function CodeView() {
|
|
const { currentProject } = useAppStore();
|
|
const [fileTree, setFileTree] = useState<FileTreeNode[]>([]);
|
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
const [fileContent, setFileContent] = useState<string>('');
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
|
|
|
// Load directory tree
|
|
const loadTree = useCallback(async () => {
|
|
if (!currentProject) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const api = getElectronAPI();
|
|
const result = await api.readdir(currentProject.path);
|
|
|
|
if (result.success && result.entries) {
|
|
const entries = result.entries
|
|
.filter((e) => !shouldIgnore(e.name))
|
|
.sort((a, b) => {
|
|
// Directories first
|
|
if (a.isDirectory && !b.isDirectory) return -1;
|
|
if (!a.isDirectory && b.isDirectory) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
})
|
|
.map((e) => ({
|
|
name: e.name,
|
|
path: `${currentProject.path}/${e.name}`,
|
|
isDirectory: e.isDirectory,
|
|
}));
|
|
|
|
setFileTree(entries);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to load file tree:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [currentProject]);
|
|
|
|
useEffect(() => {
|
|
loadTree();
|
|
}, [loadTree]);
|
|
|
|
// Load subdirectory
|
|
const loadSubdirectory = async (path: string): Promise<FileTreeNode[]> => {
|
|
try {
|
|
const api = getElectronAPI();
|
|
const result = await api.readdir(path);
|
|
|
|
if (result.success && result.entries) {
|
|
return result.entries
|
|
.filter((e) => !shouldIgnore(e.name))
|
|
.sort((a, b) => {
|
|
if (a.isDirectory && !b.isDirectory) return -1;
|
|
if (!a.isDirectory && b.isDirectory) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
})
|
|
.map((e) => ({
|
|
name: e.name,
|
|
path: `${path}/${e.name}`,
|
|
isDirectory: e.isDirectory,
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to load subdirectory:', error);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
// Load file content
|
|
const loadFileContent = async (path: string) => {
|
|
try {
|
|
const api = getElectronAPI();
|
|
const result = await api.readFile(path);
|
|
|
|
if (result.success && result.content) {
|
|
setFileContent(result.content);
|
|
setSelectedFile(path);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to load file:', error);
|
|
}
|
|
};
|
|
|
|
// Toggle folder expansion
|
|
const toggleFolder = async (node: FileTreeNode) => {
|
|
const newExpanded = new Set(expandedFolders);
|
|
|
|
if (expandedFolders.has(node.path)) {
|
|
newExpanded.delete(node.path);
|
|
} else {
|
|
newExpanded.add(node.path);
|
|
|
|
// Load children if not already loaded
|
|
if (!node.children) {
|
|
const children = await loadSubdirectory(node.path);
|
|
// Update the tree with children
|
|
const updateTree = (nodes: FileTreeNode[]): FileTreeNode[] => {
|
|
return nodes.map((n) => {
|
|
if (n.path === node.path) {
|
|
return { ...n, children };
|
|
}
|
|
if (n.children) {
|
|
return { ...n, children: updateTree(n.children) };
|
|
}
|
|
return n;
|
|
});
|
|
};
|
|
setFileTree(updateTree(fileTree));
|
|
}
|
|
}
|
|
|
|
setExpandedFolders(newExpanded);
|
|
};
|
|
|
|
// Render file tree node
|
|
const renderNode = (node: FileTreeNode, depth: number = 0) => {
|
|
const isExpanded = expandedFolders.has(node.path);
|
|
const isSelected = selectedFile === node.path;
|
|
|
|
return (
|
|
<div key={node.path}>
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50',
|
|
isSelected && 'bg-muted'
|
|
)}
|
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
onClick={() => {
|
|
if (node.isDirectory) {
|
|
toggleFolder(node);
|
|
} else {
|
|
loadFileContent(node.path);
|
|
}
|
|
}}
|
|
data-testid={`file-tree-item-${node.name}`}
|
|
>
|
|
{node.isDirectory ? (
|
|
<>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
) : (
|
|
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
)}
|
|
{isExpanded ? (
|
|
<FolderOpen className="w-4 h-4 text-primary shrink-0" />
|
|
) : (
|
|
<Folder className="w-4 h-4 text-primary shrink-0" />
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<span className="w-4" />
|
|
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
|
</>
|
|
)}
|
|
<span className="text-sm truncate">{node.name}</span>
|
|
</div>
|
|
{node.isDirectory && isExpanded && node.children && (
|
|
<div>{node.children.map((child) => renderNode(child, depth + 1))}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (!currentProject) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center" data-testid="code-view-no-project">
|
|
<p className="text-muted-foreground">No project selected</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center" data-testid="code-view-loading">
|
|
<Spinner size="lg" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="code-view">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
|
|
<div className="flex items-center gap-3">
|
|
<Code className="w-5 h-5 text-muted-foreground" />
|
|
<div>
|
|
<h1 className="text-xl font-bold">Code Explorer</h1>
|
|
<p className="text-sm text-muted-foreground">{currentProject.name}</p>
|
|
</div>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={loadTree} data-testid="refresh-tree">
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Split View */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* File Tree */}
|
|
<div className="w-64 border-r overflow-y-auto" data-testid="file-tree">
|
|
<div className="p-2">{fileTree.map((node) => renderNode(node))}</div>
|
|
</div>
|
|
|
|
{/* Code Preview */}
|
|
<div className="flex-1 overflow-hidden">
|
|
{selectedFile ? (
|
|
<div className="h-full flex flex-col">
|
|
<div className="px-4 py-2 border-b bg-muted/30">
|
|
<p className="text-sm font-mono text-muted-foreground truncate">
|
|
{selectedFile.replace(currentProject.path, '')}
|
|
</p>
|
|
</div>
|
|
<Card className="flex-1 m-4 overflow-hidden">
|
|
<CardContent className="p-0 h-full">
|
|
<pre className="p-4 h-full overflow-auto text-sm font-mono whitespace-pre-wrap">
|
|
<code data-testid="code-content">{fileContent}</code>
|
|
</pre>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<p className="text-muted-foreground">Select a file to view its contents</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|