/** * WorkspaceOutputShelf.tsx — S612: pannello output sotto l'editor * * Desktop: pannello resizable (react-resizable-panels) sotto l'editor. * Mobile: shelf collassabile da bottom con CSS transition. * * Mostra le ultime azioni del task attivo (tool calls, file writes, ecc.) * e l'ultimo run code snippet — senza duplicare TerminalPanel/xterm. * "→ Terminale" / "→ Esegui" navigano ai tab dedicati per il full output. * * S614 — bug fix + polish: * - usa selectActiveTask (evita triple subscription con tasks.find) * - auto-scroll al fondo della activity list quando arrivano nuove azioni * - MobileOutputShelf si auto-apre quando isStreaming passa da false → true */ import { useState, useEffect, useRef, useCallback, memo } from "react"; import { ChevronUp, ChevronDown, Terminal, Play, Zap, X } from "lucide-react"; import { useTaskStore, selectActiveTask } from "@/store/taskStore"; import { useChatStore } from "@/store/chatStore"; import { useWorkspaceStore } from "@/store/workspaceStore"; import { useAgentFileChanged } from "@/hooks/useAgentFileChanged"; // Fix D: watch mode import { runVfsTsCheck } from "@/lib/vfsTsChecker"; // GAP-3: TS check import type { VfsTsError } from "@/lib/vfsTsChecker"; // ─── Repair prompt formatter ────────────────────────────────────────────────── // Produce un prompt conciso che l'agente può leggere e usare per correggere // gli errori TypeScript. Max 8 errori + 4 warning per non eccedere il context. function formatRepairPrompt(errors: VfsTsError[]): string { const errs = errors.filter(e => e.sev === "error"); const warns = errors.filter(e => e.sev !== "error"); const header = errs.length > 0 && warns.length > 0 ? `🔧 Correggi ${errs.length} errore/i TypeScript e ${warns.length} warning:` : errs.length > 0 ? `🔧 Correggi ${errs.length} errore/i TypeScript:` : `🔧 Correggi ${warns.length} warning TypeScript:`; const errLines = errs.slice(0, 8).map(e => `• ${e.file}:${e.line}:${e.col} TS${e.code}: ${e.message}`); const warnLines = warns.slice(0, 4).map(e => `⚠ ${e.file}:${e.line}:${e.col} TS${e.code}: ${e.message}`); const overflow = errs.length > 8 ? [`… e altri ${errs.length - 8} errori non mostrati`] : []; return [header, "", ...errLines, ...overflow, ...(warns.length > 0 && errs.length < 8 ? warnLines : [])].join("\n"); } // ─── Action icon map ────────────────────────────────────────────────────────── const ACTION_ICONS: Record = { file_write: "✏️", file_read: "📖", file_open: "📂", file_delete: "🗑️", search_web: "🔍", code_run: "▶️", tool_call: "🔧", api_call: "🌐", thinking: "💭", plan: "📋", memory: "🧠", }; function actionIcon(type: string): string { return ACTION_ICONS[type] ?? "⚡"; } function elapsedLabel(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m`; } // ─── Tab type ───────────────────────────────────────────────────────────────── type ShelfTab = "activity" | "code" | "errors"; // ─── Shelf content ──────────────────────────────────────────────────────────── function ShelfContent({ tab, onGoToTerminal, onGoToRun, // S614: riceve activeTask come prop — un'unica subscription nel parent activeTask, agentLiveStatus, isStreaming, tsErrors, tsChecking, onRepairTs, }: { tab: ShelfTab; onGoToTerminal: () => void; onGoToRun: () => void; activeTask: ReturnType; agentLiveStatus: string; isStreaming: boolean; tsErrors: VfsTsError[]; tsChecking: boolean; /** Fix D: callback che inietta il repair prompt nell'input bar via "agente:submit_input" */ onRepairTs?: () => void; }) { const runCode = useWorkspaceStore(s => s.runCode); // S614: auto-scroll al fondo quando arrivano nuove azioni const bottomRef = useRef(null); const actions = activeTask?.actions ?? []; useEffect(() => { if (tab === "activity" && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); } }, [actions.length, tab]); if (tab === "activity") { const MAX_VISIBLE = 20; const hiddenCount = Math.max(0, actions.length - MAX_VISIBLE); const recentActions = actions.slice(-MAX_VISIBLE); // ultimi 20, ordine cronologico return (
{/* S621: indicatore azioni precedenti nascosti (truncation hint) */} {hiddenCount > 0 && (
⋯ {hiddenCount} azioni precedenti
)} {/* Live status */} {isStreaming && agentLiveStatus && (
{agentLiveStatus}
)} {/* Actions log */} {recentActions.length === 0 ? (

Nessuna attività. L'agente inizierà a registrare le azioni qui.

) : ( recentActions.map(action => (
{actionIcon(action.type)} {action.explanation ?? action.label} {action.detail && ( {action.detail} )} {action.durationMs !== undefined && ( {elapsedLabel(action.durationMs)} )}
)) )} {/* S614: sentinella per auto-scroll */}
{/* Full terminal link */}
); } // ─── Tab ERRORI — Fix D: TS errors live aggiornati dopo ogni file write ── if (tab === "errors") { return (
{tsChecking && (
Controllo TypeScript…
)} {onRepairTs && tsErrors.length > 0 && ( )} {!tsChecking && tsErrors.length === 0 ? (

✓ Nessun errore TypeScript rilevato.

) : ( tsErrors.map((err, i) => (
{err.sev === "error" ? "ERR" : "WARN"} {err.file.split("/").pop()}:{err.line}:{err.col} TS{err.code}
{err.message}
)) )}
); } // tab === "code" return (
{runCode ? ( <>
{runCode.filename}
            {runCode.code.slice(0, 800)}
            {runCode.code.length > 800 && "\n… (troncato)"}
          
) : (

Nessun codice in esecuzione. L'agente popolerà questo pannello quando eseguirà codice.

)}
); } // ─── Desktop shelf (embedded in PanelGroup) ─────────────────────────────────── export const WorkspaceOutputShelf = memo(function WorkspaceOutputShelf({ onGoToTerminal, onGoToRun, }: { onGoToTerminal: () => void; onGoToRun: () => void; }) { const [tab, setTab] = useState("activity"); // S614: una sola subscription per il task attivo const activeTask = useTaskStore(selectActiveTask); const isStreaming = useChatStore(s => s.isStreaming); const agentLiveStatus= useChatStore(s => s.agentLiveStatus); const runCode = useWorkspaceStore(s => s.runCode); const activityCount = activeTask?.actions.length ?? 0; const hasCode = !!runCode; const setInput = useChatStore(s => s.setInput); // Fix D: repair bridge // ─── Fix D: TS errors live — aggiornati dopo ogni file write (agent:file-changed) ── const [tsErrors, setTsErrors] = useState([]); const [tsChecking, setTsChecking] = useState(false); const _mountedD = useRef(true); useEffect(() => { _mountedD.current = true; return () => { _mountedD.current = false; }; }, []); const _runTsCheck = useCallback(async () => { setTsChecking(true); try { const errs = await runVfsTsCheck(); if (_mountedD.current) setTsErrors(errs); } catch { /* non-fatal */ } finally { if (_mountedD.current) setTsChecking(false); } }, []); // Debounce 600ms — leggermente più lento del debounce del hook (300ms) per // aspettare che tutte le scritture batch siano complete prima del TS check. useAgentFileChanged(useCallback(() => { void _runTsCheck(); }, [_runTsCheck]), 600); // ─── Fix D: onRepairTs — invia errori TS all'agente via ChatInputBar bridge ── const _onRepairTs = useCallback(() => { if (!tsErrors.length) return; setInput(formatRepairPrompt(tsErrors)); window.dispatchEvent(new CustomEvent("agente:submit_input")); }, [tsErrors, setInput]); return (
{/* Header + tabs */}
{([ { id: "activity" as ShelfTab, label: "ATTIVITÀ", badge: activityCount, badgeColor: isStreaming ? "#22c55e" : "#3b82f6" }, { id: "code" as ShelfTab, label: "CODICE", badge: hasCode ? 1 : 0, badgeColor: "#3b82f6" }, { id: "errors" as ShelfTab, label: "ERRORI", badge: tsErrors.length, badgeColor: tsErrors.some(e=>e.sev==="error") ? "#ef4444" : "#f59e0b" }, ] as const).map(t => ( ))} {/* Right spacer + links */}
{/* Content — S614: passa activeTask/isStreaming come props, evita triple subscription */}
); }); // ─── Mobile collapseable shelf ──────────────────────────────────────────────── export const MobileOutputShelf = memo(function MobileOutputShelf({ onGoToTerminal, onGoToRun, }: { onGoToTerminal: () => void; onGoToRun: () => void; }) { const [open, setOpen] = useState(false); const [tab, setTab] = useState("activity"); // S614: una sola subscription const activeTask = useTaskStore(selectActiveTask); const isStreaming = useChatStore(s => s.isStreaming); const agentLiveStatus= useChatStore(s => s.agentLiveStatus); const actCount = activeTask?.actions.length ?? 0; const setInputM = useChatStore(s => s.setInput); // Fix D: repair bridge // ─── Fix D: TS errors live (condivide la stessa logica del desktop) ────── const [tsErrors, setTsErrorsM] = useState([]); const [tsCheckingM, setTsCheckingM] = useState(false); const _mountedM = useRef(true); useEffect(() => { _mountedM.current = true; return () => { _mountedM.current = false; }; }, []); const _runTsCheckM = useCallback(async () => { setTsCheckingM(true); try { const errs = await runVfsTsCheck(); if (_mountedM.current) setTsErrorsM(errs); } catch { /* non-fatal */ } finally { if (_mountedM.current) setTsCheckingM(false); } }, []); useAgentFileChanged(useCallback(() => { void _runTsCheckM(); }, [_runTsCheckM]), 600); const _onRepairTsM = useCallback(() => { if (!tsErrors.length) return; setInputM(formatRepairPrompt(tsErrors)); window.dispatchEvent(new CustomEvent("agente:submit_input")); }, [tsErrors, setInputM]); // S614: auto-apre il pannello quando l'agente inizia a streammare const prevStreaming = useRef(false); useEffect(() => { if (isStreaming && !prevStreaming.current) { setOpen(true); } prevStreaming.current = isStreaming; }, [isStreaming]); return (
{/* Toggle bar */} {/* Shelf content when open */} {open && (
{/* Tabs */}
{(["activity", "code", "errors"] as ShelfTab[]).map(t => ( ))}
{ onGoToTerminal(); setOpen(false); }} onGoToRun={() => { onGoToRun(); setOpen(false); }} activeTask={activeTask} agentLiveStatus={agentLiveStatus} isStreaming={isStreaming} tsErrors={tsErrors} tsChecking={tsCheckingM} onRepairTs={_onRepairTsM} />
)}
); });