/** * RecentFileTabs.tsx — S613: tab strip file recenti sopra il code editor * * Mostra gli ultimi MAX_RECENT file aperti come tab cliccabili (pattern VS Code). * Integrato nella riga sopra FileEditor nel top Panel del PanelGroup S612. * * - Click tab → apre il file (setOpenFile + setRequestOpenPath) * - × → chiude il tab (closeRecentFile; se era il file attivo, apre il precedente) * - Tab attivo evidenziato in blu * - Overflow orizzontale (no wrap) con scroll invisibile * * S614: import type { CSSProperties, RefObject } — import diretto da "react", non prefissato * data-path su FileTab per useActiveTabScroll * auto-scroll del tab attivo nella viewport * * S616: badge arancione "●" sui tab modificati dall'agente mentre erano in background. * onVfsChanged → markDirty(path) per file in recentFiles che non sono aperti. * Apertura tab → clearDirty(path) automatica via setOpenFile. */ import { useRef, useEffect, memo } from "react"; import type { CSSProperties, RefObject } from "react"; import { X } from "lucide-react"; import { useWorkspaceStore } from "@/store/workspaceStore"; import { onVfsFileChanged } from "@/lib/vfsDb"; // S619: usa onVfsFileChanged per dirty badge preciso import type { VFSFile } from "@/lib/vfsDb"; // ─── Icon helpers ───────────────────────────────────────────────────────────── function fileIcon(name: string): string { if (name.endsWith(".py")) return "🐍"; if (name.endsWith(".ts") || name.endsWith(".tsx")) return "📘"; if (name.endsWith(".js") || name.endsWith(".jsx")) return "📜"; if (name.endsWith(".json")) return "📋"; if (name.endsWith(".html")) return "🌐"; if (name.endsWith(".css") || name.endsWith(".scss")) return "🎨"; if (name.endsWith(".md")) return "📝"; if (name.endsWith(".sh")) return "⚙️"; return "📄"; } // ─── Single tab ─────────────────────────────────────────────────────────────── function FileTab({ file, isActive, isDirty, onOpen, onClose, }: { file: VFSFile; isActive: boolean; isDirty: boolean; onOpen: (f: VFSFile) => void; onClose: (path: string) => void; }) { return (
onOpen(file)} title={isDirty ? `${file.path} — modificato dall'agente` : file.path} style={{ display: "flex", alignItems: "center", gap: 4, padding: "0 10px 0 8px", height: "100%", flexShrink: 0, cursor: "pointer", background: isActive ? "rgba(99,102,241,0.12)" : "transparent", borderRight: "1px solid rgba(99,102,241,0.08)", borderBottom: isActive ? "2px solid #818cf8" : "2px solid transparent", color: isActive ? "#c7d2fe" : "#4a4a78", fontSize: "0.68rem", transition: "color 0.15s, background 0.15s, border-color 0.15s", userSelect: "none", maxWidth: 160, minWidth: 60, position: "relative", }} onMouseEnter={e => { if (!isActive) { e.currentTarget.style.color = "#818cf8"; e.currentTarget.style.background = "rgba(99,102,241,0.06)"; } }} onMouseLeave={e => { if (!isActive) { e.currentTarget.style.color = "#4a4a78"; e.currentTarget.style.background = "transparent"; } }} > {/* S616: dirty dot — arancione, pulsa quando il file è stato modificato */} {isDirty && !isActive && ( )} {fileIcon(file.name)} {file.name}
); } // ─── Tab strip ──────────────────────────────────────────────────────────────── // ─── Active tab auto-scroll ────────────────────────────────────────────────── S614 function useActiveTabScroll( containerRef: RefObject, activePath: string | null, ) { useEffect(() => { if (!containerRef.current || !activePath) return; const el = containerRef.current.querySelector( `[data-path="${CSS.escape(activePath)}"]`, ) as HTMLElement | null; el?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }); }, [activePath, containerRef]); } export const RecentFileTabs = memo(function RecentFileTabs() { const recentFiles = useWorkspaceStore(s => s.recentFiles); const openFile = useWorkspaceStore(s => s.openFile); const dirtyPaths = useWorkspaceStore(s => s.dirtyPaths); // S616 const setOpenFile = useWorkspaceStore(s => s.setOpenFile); const setRequestOpenPath = useWorkspaceStore(s => s.setRequestOpenPath); const closeRecentFile = useWorkspaceStore(s => s.closeRecentFile); const containerRef = useRef(null); // S614: scroll il tab attivo nella viewport quando cambia useActiveTabScroll(containerRef, openFile?.path ?? null); // S619: usa onVfsFileChanged per dirty badge preciso — segna solo il file effettivamente modificato // (fix S616: era onVfsChanged che marcava TUTTI i tab non-attivi ad ogni write) useEffect(() => { return onVfsFileChanged((changedPath) => { const { recentFiles: rf, openFile: of, markDirty: md } = useWorkspaceStore.getState(); // Marca dirty solo se il file è nei tab recenti E non è quello attivo const isRecent = rf.some(f => f.path === changedPath); if (isRecent && of?.path !== changedPath) { md(changedPath); } }); }, []); // Non renderizzare se non ci sono file recenti if (recentFiles.length === 0) return null; const handleOpen = (file: VFSFile) => { setOpenFile(file); // S616: clearDirty avviene dentro setOpenFile setRequestOpenPath(file.path); }; const handleClose = (path: string) => { closeRecentFile(path); // S616: clearDirty avviene dentro closeRecentFile }; return (
{recentFiles.map(file => ( ))}
); });