diff --git a/server/claude-sdk.js b/server/claude-sdk.js index d5e4cc8..ea47c37 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -538,7 +538,7 @@ async function queryClaudeSDK(command, options = {}, ws) { return { behavior: 'deny', message: decision.message ?? 'User denied tool use' }; }; - // Set stream-close timeout for interactive tools (Query constructor reads it synchronously) + // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT; process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000'; diff --git a/src/components/DiffViewer.jsx b/src/components/DiffViewer.jsx index a624c0b..a6d963a 100644 --- a/src/components/DiffViewer.jsx +++ b/src/components/DiffViewer.jsx @@ -3,7 +3,7 @@ import React from 'react'; function DiffViewer({ diff, fileName, isMobile, wrapText }) { if (!diff) { return ( -
+
No diff available
); @@ -17,13 +17,13 @@ function DiffViewer({ diff, fileName, isMobile, wrapText }) { return (
{line} diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx index 643d8eb..c966fcf 100644 --- a/src/components/FileTree.jsx +++ b/src/components/FileTree.jsx @@ -3,12 +3,263 @@ import { useTranslation } from 'react-i18next'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { Input } from './ui/input'; -import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X } from 'lucide-react'; +import { + Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X, + ChevronRight, + FileJson, FileType, FileSpreadsheet, FileArchive, + Hash, Braces, Terminal, Database, Globe, Palette, Music2, Video, Archive, + Lock, Shield, Settings, Image, BookOpen, Cpu, Box, Gem, Coffee, + Flame, Hexagon, FileCode2, Code2, Cog, FileWarning, Binary, SquareFunction, + Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks +} from 'lucide-react'; import { cn } from '../lib/utils'; import CodeEditor from './CodeEditor'; import ImageViewer from './ImageViewer'; import { api } from '../utils/api'; +// ─── File Icon Registry ────────────────────────────────────────────── +// Maps file extensions (and special filenames) to { icon, colorClass } pairs. +// Uses lucide-react icons mapped semantically to file types. + +const ICON_SIZE = 'w-4 h-4 flex-shrink-0'; + +const FILE_ICON_MAP = { + // ── JavaScript / TypeScript ── + js: { icon: FileCode, color: 'text-yellow-500' }, + jsx: { icon: FileCode, color: 'text-yellow-500' }, + mjs: { icon: FileCode, color: 'text-yellow-500' }, + cjs: { icon: FileCode, color: 'text-yellow-500' }, + ts: { icon: FileCode2, color: 'text-blue-500' }, + tsx: { icon: FileCode2, color: 'text-blue-500' }, + mts: { icon: FileCode2, color: 'text-blue-500' }, + + // ── Python ── + py: { icon: Code2, color: 'text-emerald-500' }, + pyw: { icon: Code2, color: 'text-emerald-500' }, + pyi: { icon: Code2, color: 'text-emerald-400' }, + ipynb:{ icon: NotebookPen, color: 'text-orange-500' }, + + // ── Rust ── + rs: { icon: Cog, color: 'text-orange-600' }, + toml: { icon: Settings, color: 'text-gray-500' }, + + // ── Go ── + go: { icon: Hexagon, color: 'text-cyan-500' }, + + // ── Ruby ── + rb: { icon: Gem, color: 'text-red-500' }, + erb: { icon: Gem, color: 'text-red-400' }, + + // ── PHP ── + php: { icon: Blocks, color: 'text-violet-500' }, + + // ── Java / Kotlin ── + java: { icon: Coffee, color: 'text-red-600' }, + jar: { icon: Coffee, color: 'text-red-500' }, + kt: { icon: Hexagon, color: 'text-violet-500' }, + kts: { icon: Hexagon, color: 'text-violet-400' }, + + // ── C / C++ ── + c: { icon: Cpu, color: 'text-blue-600' }, + h: { icon: Cpu, color: 'text-blue-400' }, + cpp: { icon: Cpu, color: 'text-blue-700' }, + hpp: { icon: Cpu, color: 'text-blue-500' }, + cc: { icon: Cpu, color: 'text-blue-700' }, + + // ── C# ── + cs: { icon: Hexagon, color: 'text-purple-600' }, + + // ── Swift ── + swift:{ icon: Flame, color: 'text-orange-500' }, + + // ── Lua ── + lua: { icon: SquareFunction, color: 'text-blue-500' }, + + // ── R ── + r: { icon: FlaskConical, color: 'text-blue-600' }, + + // ── Web ── + html: { icon: Globe, color: 'text-orange-600' }, + htm: { icon: Globe, color: 'text-orange-600' }, + css: { icon: Hash, color: 'text-blue-500' }, + scss: { icon: Hash, color: 'text-pink-500' }, + sass: { icon: Hash, color: 'text-pink-400' }, + less: { icon: Hash, color: 'text-indigo-500' }, + vue: { icon: FileCode2, color: 'text-emerald-500' }, + svelte:{ icon: FileCode2, color: 'text-orange-500' }, + + // ── Data / Config ── + json: { icon: Braces, color: 'text-yellow-600' }, + jsonc:{ icon: Braces, color: 'text-yellow-500' }, + json5:{ icon: Braces, color: 'text-yellow-500' }, + yaml: { icon: Settings, color: 'text-purple-400' }, + yml: { icon: Settings, color: 'text-purple-400' }, + xml: { icon: FileCode, color: 'text-orange-500' }, + csv: { icon: FileSpreadsheet, color: 'text-green-600' }, + tsv: { icon: FileSpreadsheet, color: 'text-green-500' }, + sql: { icon: Database, color: 'text-blue-500' }, + graphql:{ icon: Workflow, color: 'text-pink-500' }, + gql: { icon: Workflow, color: 'text-pink-500' }, + proto:{ icon: Box, color: 'text-green-500' }, + env: { icon: Shield, color: 'text-yellow-600' }, + + // ── Documents ── + md: { icon: BookOpen, color: 'text-blue-500' }, + mdx: { icon: BookOpen, color: 'text-blue-400' }, + txt: { icon: FileText, color: 'text-gray-500' }, + doc: { icon: FileText, color: 'text-blue-600' }, + docx: { icon: FileText, color: 'text-blue-600' }, + pdf: { icon: FileCheck, color: 'text-red-600' }, + rtf: { icon: FileText, color: 'text-gray-500' }, + tex: { icon: Scroll, color: 'text-teal-600' }, + rst: { icon: FileText, color: 'text-gray-400' }, + + // ── Shell / Scripts ── + sh: { icon: Terminal, color: 'text-green-500' }, + bash: { icon: Terminal, color: 'text-green-500' }, + zsh: { icon: Terminal, color: 'text-green-400' }, + fish: { icon: Terminal, color: 'text-green-400' }, + ps1: { icon: Terminal, color: 'text-blue-400' }, + bat: { icon: Terminal, color: 'text-gray-500' }, + cmd: { icon: Terminal, color: 'text-gray-500' }, + + // ── Images ── + png: { icon: Image, color: 'text-purple-500' }, + jpg: { icon: Image, color: 'text-purple-500' }, + jpeg: { icon: Image, color: 'text-purple-500' }, + gif: { icon: Image, color: 'text-purple-400' }, + webp: { icon: Image, color: 'text-purple-400' }, + ico: { icon: Image, color: 'text-purple-400' }, + bmp: { icon: Image, color: 'text-purple-400' }, + tiff: { icon: Image, color: 'text-purple-400' }, + svg: { icon: Palette, color: 'text-amber-500' }, + + // ── Audio ── + mp3: { icon: Music2, color: 'text-pink-500' }, + wav: { icon: Music2, color: 'text-pink-500' }, + ogg: { icon: Music2, color: 'text-pink-400' }, + flac: { icon: Music2, color: 'text-pink-400' }, + aac: { icon: Music2, color: 'text-pink-400' }, + m4a: { icon: Music2, color: 'text-pink-400' }, + + // ── Video ── + mp4: { icon: Video, color: 'text-rose-500' }, + mov: { icon: Video, color: 'text-rose-500' }, + avi: { icon: Video, color: 'text-rose-500' }, + webm: { icon: Video, color: 'text-rose-400' }, + mkv: { icon: Video, color: 'text-rose-400' }, + + // ── Fonts ── + ttf: { icon: FileType, color: 'text-red-500' }, + otf: { icon: FileType, color: 'text-red-500' }, + woff: { icon: FileType, color: 'text-red-400' }, + woff2:{ icon: FileType, color: 'text-red-400' }, + eot: { icon: FileType, color: 'text-red-400' }, + + // ── Archives ── + zip: { icon: Archive, color: 'text-amber-600' }, + tar: { icon: Archive, color: 'text-amber-600' }, + gz: { icon: Archive, color: 'text-amber-600' }, + bz2: { icon: Archive, color: 'text-amber-600' }, + rar: { icon: Archive, color: 'text-amber-500' }, + '7z': { icon: Archive, color: 'text-amber-500' }, + + // ── Lock files ── + lock: { icon: Lock, color: 'text-gray-500' }, + + // ── Binary / Executable ── + exe: { icon: Binary, color: 'text-gray-500' }, + bin: { icon: Binary, color: 'text-gray-500' }, + dll: { icon: Binary, color: 'text-gray-400' }, + so: { icon: Binary, color: 'text-gray-400' }, + dylib:{ icon: Binary, color: 'text-gray-400' }, + wasm: { icon: Binary, color: 'text-purple-500' }, + + // ── Misc config ── + ini: { icon: Settings, color: 'text-gray-500' }, + cfg: { icon: Settings, color: 'text-gray-500' }, + conf: { icon: Settings, color: 'text-gray-500' }, + log: { icon: Scroll, color: 'text-gray-400' }, + map: { icon: File, color: 'text-gray-400' }, +}; + +// Special full-filename matches (highest priority) +const FILENAME_ICON_MAP = { + 'Dockerfile': { icon: Box, color: 'text-blue-500' }, + 'docker-compose.yml': { icon: Box, color: 'text-blue-500' }, + 'docker-compose.yaml': { icon: Box, color: 'text-blue-500' }, + '.dockerignore': { icon: Box, color: 'text-gray-500' }, + '.gitignore': { icon: Settings, color: 'text-gray-500' }, + '.gitmodules': { icon: Settings, color: 'text-gray-500' }, + '.gitattributes': { icon: Settings, color: 'text-gray-500' }, + '.editorconfig': { icon: Settings, color: 'text-gray-500' }, + '.prettierrc': { icon: Settings, color: 'text-pink-400' }, + '.prettierignore': { icon: Settings, color: 'text-gray-500' }, + '.eslintrc': { icon: Settings, color: 'text-violet-500' }, + '.eslintrc.js': { icon: Settings, color: 'text-violet-500' }, + '.eslintrc.json': { icon: Settings, color: 'text-violet-500' }, + '.eslintrc.cjs': { icon: Settings, color: 'text-violet-500' }, + 'eslint.config.js': { icon: Settings, color: 'text-violet-500' }, + 'eslint.config.mjs':{ icon: Settings, color: 'text-violet-500' }, + '.env': { icon: Shield, color: 'text-yellow-600' }, + '.env.local': { icon: Shield, color: 'text-yellow-600' }, + '.env.development': { icon: Shield, color: 'text-yellow-500' }, + '.env.production': { icon: Shield, color: 'text-yellow-600' }, + '.env.example': { icon: Shield, color: 'text-yellow-400' }, + 'package.json': { icon: Braces, color: 'text-green-500' }, + 'package-lock.json':{ icon: Lock, color: 'text-gray-500' }, + 'yarn.lock': { icon: Lock, color: 'text-blue-400' }, + 'pnpm-lock.yaml': { icon: Lock, color: 'text-orange-400' }, + 'bun.lockb': { icon: Lock, color: 'text-gray-400' }, + 'Cargo.toml': { icon: Cog, color: 'text-orange-600' }, + 'Cargo.lock': { icon: Lock, color: 'text-orange-400' }, + 'Gemfile': { icon: Gem, color: 'text-red-500' }, + 'Gemfile.lock': { icon: Lock, color: 'text-red-400' }, + 'Makefile': { icon: Terminal, color: 'text-gray-500' }, + 'CMakeLists.txt': { icon: Cog, color: 'text-blue-500' }, + 'tsconfig.json': { icon: Braces, color: 'text-blue-500' }, + 'jsconfig.json': { icon: Braces, color: 'text-yellow-500' }, + 'vite.config.ts': { icon: Flame, color: 'text-purple-500' }, + 'vite.config.js': { icon: Flame, color: 'text-purple-500' }, + 'webpack.config.js':{ icon: Cog, color: 'text-blue-500' }, + 'tailwind.config.js':{ icon: Hash, color: 'text-cyan-500' }, + 'tailwind.config.ts':{ icon: Hash, color: 'text-cyan-500' }, + 'postcss.config.js':{ icon: Cog, color: 'text-red-400' }, + 'babel.config.js': { icon: Settings, color: 'text-yellow-500' }, + '.babelrc': { icon: Settings, color: 'text-yellow-500' }, + 'README.md': { icon: BookOpen, color: 'text-blue-500' }, + 'LICENSE': { icon: FileCheck, color: 'text-gray-500' }, + 'LICENSE.md': { icon: FileCheck, color: 'text-gray-500' }, + 'CHANGELOG.md': { icon: Scroll, color: 'text-blue-400' }, + 'requirements.txt': { icon: FileText, color: 'text-emerald-400' }, + 'go.mod': { icon: Hexagon, color: 'text-cyan-500' }, + 'go.sum': { icon: Lock, color: 'text-cyan-400' }, +}; + +function getFileIconData(filename) { + // 1. Exact filename match + if (FILENAME_ICON_MAP[filename]) { + return FILENAME_ICON_MAP[filename]; + } + + // 2. Check for .env prefix pattern + if (filename.startsWith('.env')) { + return { icon: Shield, color: 'text-yellow-600' }; + } + + // 3. Extension-based lookup + const ext = filename.split('.').pop()?.toLowerCase(); + if (ext && FILE_ICON_MAP[ext]) { + return FILE_ICON_MAP[ext]; + } + + // 4. Fallback + return { icon: File, color: 'text-muted-foreground' }; +} + + +// ─── Component ─────────────────────────────────────────────────────── + function FileTree({ selectedProject }) { const { t } = useTranslation(); const [files, setFiles] = useState([]); @@ -16,7 +267,7 @@ function FileTree({ selectedProject }) { const [expandedDirs, setExpandedDirs] = useState(new Set()); const [selectedFile, setSelectedFile] = useState(null); const [selectedImage, setSelectedImage] = useState(null); - const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact' + const [viewMode, setViewMode] = useState('detailed'); const [searchQuery, setSearchQuery] = useState(''); const [filteredFiles, setFilteredFiles] = useState([]); @@ -26,7 +277,6 @@ function FileTree({ selectedProject }) { } }, [selectedProject]); - // Load view mode preference from localStorage useEffect(() => { const savedViewMode = localStorage.getItem('file-tree-view-mode'); if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) { @@ -34,7 +284,6 @@ function FileTree({ selectedProject }) { } }, []); - // Filter files based on search query useEffect(() => { if (!searchQuery.trim()) { setFilteredFiles(files); @@ -42,7 +291,6 @@ function FileTree({ selectedProject }) { const filtered = filterFiles(files, searchQuery.toLowerCase()); setFilteredFiles(filtered); - // Auto-expand directories that contain matches const expandMatches = (items) => { items.forEach(item => { if (item.type === 'directory' && item.children && item.children.length > 0) { @@ -55,7 +303,6 @@ function FileTree({ selectedProject }) { } }, [files, searchQuery]); - // Recursively filter files and directories based on search query const filterFiles = (items, query) => { return items.reduce((filtered, item) => { const matchesName = item.name.toLowerCase().includes(query); @@ -65,9 +312,6 @@ function FileTree({ selectedProject }) { filteredChildren = filterFiles(item.children, query); } - // Include item if: - // 1. It matches the search query, or - // 2. It's a directory with matching children if (matchesName || filteredChildren.length > 0) { filtered.push({ ...item, @@ -83,14 +327,14 @@ function FileTree({ selectedProject }) { setLoading(true); try { const response = await api.getFiles(selectedProject.name); - + if (!response.ok) { const errorText = await response.text(); console.error('❌ File fetch failed:', response.status, errorText); setFiles([]); return; } - + const data = await response.json(); setFiles(data); } catch (error) { @@ -111,13 +355,11 @@ function FileTree({ selectedProject }) { setExpandedDirs(newExpanded); }; - // Change view mode and save preference const changeViewMode = (mode) => { setViewMode(mode); localStorage.setItem('file-tree-view-mode', mode); }; - // Format file size const formatFileSize = (bytes) => { if (!bytes || bytes === 0) return '0 B'; const k = 1024; @@ -126,7 +368,6 @@ function FileTree({ selectedProject }) { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; - // Format date as relative time const formatRelativeTime = (date) => { if (!date) return '-'; const now = new Date(); @@ -140,65 +381,6 @@ function FileTree({ selectedProject }) { return past.toLocaleDateString(); }; - const renderFileTree = (items, level = 0) => { - return items.map((item) => ( -
- - - {item.type === 'directory' && - expandedDirs.has(item.path) && - item.children && - item.children.length > 0 && ( -
- {renderFileTree(item.children, level + 1)} -
- )} -
- )); - }; - const isImageFile = (filename) => { const ext = filename.split('.').pop()?.toLowerCase(); const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp']; @@ -206,208 +388,289 @@ function FileTree({ selectedProject }) { }; const getFileIcon = (filename) => { - const ext = filename.split('.').pop()?.toLowerCase(); - - const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs']; - const docExtensions = ['md', 'txt', 'doc', 'pdf']; - const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp']; - - if (codeExtensions.includes(ext)) { - return ; - } else if (docExtensions.includes(ext)) { - return ; - } else if (imageExtensions.includes(ext)) { - return ; + const { icon: Icon, color } = getFileIconData(filename); + return ; + }; + + // ── Click handler shared across all view modes ── + const handleItemClick = (item) => { + if (item.type === 'directory') { + toggleDirectory(item.path); + } else if (isImageFile(item.name)) { + setSelectedImage({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); } else { - return ; + setSelectedFile({ + name: item.name, + path: item.path, + projectPath: selectedProject.path, + projectName: selectedProject.name + }); } }; - // Render detailed view with table-like layout + // ── Indent guide + folder/file icon rendering ── + const renderIndentGuides = (level) => { + if (level === 0) return null; + return ( +