Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, string> = { | |
| 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<typeof selectActiveTask>; | |
| 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<HTMLDivElement | null>(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 ( | |
| <div style={{ flex: 1, overflow: "auto", padding: "4px 0" }}> | |
| {/* S621: indicatore azioni precedenti nascosti (truncation hint) */} | |
| {hiddenCount > 0 && ( | |
| <div style={{ | |
| display: "flex", alignItems: "center", justifyContent: "center", | |
| padding: "3px 10px", | |
| borderBottom: "1px solid rgba(99,102,241,0.08)", | |
| marginBottom: 2, | |
| }}> | |
| <span style={{ fontSize: "0.62rem", color: "#4a4a78", fontStyle: "italic" }}> | |
| ⋯ {hiddenCount} azioni precedenti | |
| </span> | |
| </div> | |
| )} | |
| {/* Live status */} | |
| {isStreaming && agentLiveStatus && ( | |
| <div style={{ | |
| display: "flex", alignItems: "center", gap: 6, | |
| padding: "4px 10px", | |
| borderBottom: "1px solid rgba(99,102,241,0.10)", | |
| marginBottom: 2, | |
| }}> | |
| <span style={{ | |
| width: 6, height: 6, borderRadius: "50%", | |
| background: "#22c55e", | |
| animation: "mem-tab-pulse 1s ease-in-out infinite", | |
| flexShrink: 0, | |
| }} /> | |
| <span style={{ fontSize: "0.7rem", color: "#86efac", fontStyle: "italic", | |
| overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}> | |
| {agentLiveStatus} | |
| </span> | |
| </div> | |
| )} | |
| {/* Actions log */} | |
| {recentActions.length === 0 ? ( | |
| <p style={{ fontSize: "0.68rem", color: "#3a3a6a", padding: "10px 12px", lineHeight: 1.5 }}> | |
| Nessuna attività. L'agente inizierà a registrare le azioni qui. | |
| </p> | |
| ) : ( | |
| recentActions.map(action => ( | |
| <div key={action.id} style={{ | |
| display: "flex", alignItems: "center", gap: 6, | |
| padding: "2px 10px", | |
| opacity: action.done ? 0.6 : 1, | |
| transition: "opacity 0.2s", | |
| }}> | |
| <span style={{ fontSize: "0.7rem", flexShrink: 0 }}>{actionIcon(action.type)}</span> | |
| <span style={{ | |
| flex: 1, fontSize: "0.68rem", | |
| color: action.done ? "#4a5568" : "#a0aec0", | |
| overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", | |
| }}> | |
| {action.explanation ?? action.label} | |
| </span> | |
| {action.detail && ( | |
| <span style={{ fontSize: "0.62rem", color: "#3b82f6", | |
| maxWidth: 100, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", | |
| flexShrink: 0 }}> | |
| {action.detail} | |
| </span> | |
| )} | |
| {action.durationMs !== undefined && ( | |
| <span style={{ fontSize: "0.6rem", color: "#2d3748", flexShrink: 0 }}> | |
| {elapsedLabel(action.durationMs)} | |
| </span> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| {/* S614: sentinella per auto-scroll */} | |
| <div ref={bottomRef} style={{ height: 1 }} /> | |
| {/* Full terminal link */} | |
| <button | |
| onClick={onGoToTerminal} | |
| style={{ | |
| display: "flex", alignItems: "center", gap: 4, | |
| margin: "6px 10px 4px", | |
| background: "none", border: "none", cursor: "pointer", | |
| color: "#4a4a78", fontSize: "0.62rem", | |
| padding: "3px 0", | |
| transition: "color 0.15s", | |
| }} | |
| onMouseEnter={e => { e.currentTarget.style.color = "#818cf8"; }} | |
| onMouseLeave={e => { e.currentTarget.style.color = "#4a4a78"; }} | |
| > | |
| <Terminal size={10} /> | |
| Apri terminale completo → | |
| </button> | |
| </div> | |
| ); | |
| } | |
| // ─── Tab ERRORI — Fix D: TS errors live aggiornati dopo ogni file write ── | |
| if (tab === "errors") { | |
| return ( | |
| <div style={{ flex: 1, overflow: "auto", padding: "6px 10px" }}> | |
| {tsChecking && ( | |
| <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 0", marginBottom: 4 }}> | |
| <span style={{ | |
| width: 6, height: 6, borderRadius: "50%", | |
| background: "#f59e0b", | |
| animation: "mem-tab-pulse 1s ease-in-out infinite", | |
| flexShrink: 0, | |
| }} /> | |
| <span style={{ fontSize: "0.65rem", color: "#fbbf24", fontStyle: "italic" }}> | |
| Controllo TypeScript… | |
| </span> | |
| </div> | |
| )} | |
| {onRepairTs && tsErrors.length > 0 && ( | |
| <button | |
| onClick={onRepairTs} | |
| disabled={isStreaming} | |
| style={{ | |
| display: "flex", alignItems: "center", gap: 5, | |
| margin: "6px 0 8px", | |
| padding: "5px 12px", | |
| background: isStreaming | |
| ? "rgba(99,102,241,0.06)" | |
| : "rgba(99,102,241,0.14)", | |
| border: "1px solid rgba(99,102,241,0.30)", | |
| borderRadius: 8, | |
| cursor: isStreaming ? "not-allowed" : "pointer", | |
| color: isStreaming ? "#3a3a6a" : "#818cf8", | |
| fontSize: "0.68rem", fontWeight: 600, | |
| transition: "background 0.15s, color 0.15s", | |
| opacity: isStreaming ? 0.5 : 1, | |
| }} | |
| onMouseEnter={e => { if (!isStreaming) e.currentTarget.style.background = "rgba(99,102,241,0.22)"; }} | |
| onMouseLeave={e => { if (!isStreaming) e.currentTarget.style.background = "rgba(99,102,241,0.14)"; }} | |
| title={isStreaming ? "Agente già in esecuzione — attendi il completamento" : `Invia ${tsErrors.length} errore/i all'agente per la correzione automatica`} | |
| > | |
| 🔧 | |
| {isStreaming ? "Agente in esecuzione…" : `Correggi ${tsErrors.length > 9 ? "9+" : tsErrors.length} errore/i`} | |
| </button> | |
| )} | |
| {!tsChecking && tsErrors.length === 0 ? ( | |
| <p style={{ fontSize: "0.68rem", color: "#22c55e", lineHeight: 1.5 }}> | |
| ✓ Nessun errore TypeScript rilevato. | |
| </p> | |
| ) : ( | |
| tsErrors.map((err, i) => ( | |
| <div key={i} style={{ | |
| display: "flex", flexDirection: "column", gap: 1, | |
| padding: "4px 0", | |
| borderBottom: "1px solid rgba(99,102,241,0.07)", | |
| }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 6 }}> | |
| <span style={{ | |
| fontSize: "0.6rem", fontWeight: 700, | |
| color: err.sev === "error" ? "#f87171" : "#fbbf24", | |
| flexShrink: 0, | |
| }}> | |
| {err.sev === "error" ? "ERR" : "WARN"} | |
| </span> | |
| <span style={{ | |
| fontSize: "0.6rem", color: "#4a4a78", fontFamily: "monospace", | |
| overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", | |
| }}> | |
| {err.file.split("/").pop()}:{err.line}:{err.col} | |
| </span> | |
| <span style={{ fontSize: "0.55rem", color: "#3a3a5a", flexShrink: 0 }}> | |
| TS{err.code} | |
| </span> | |
| </div> | |
| <span style={{ | |
| fontSize: "0.67rem", color: "#9ca3af", | |
| paddingLeft: 0, | |
| overflow: "hidden", textOverflow: "ellipsis", | |
| display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", | |
| }}> | |
| {err.message} | |
| </span> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| ); | |
| } | |
| // tab === "code" | |
| return ( | |
| <div style={{ flex: 1, overflow: "auto", padding: "6px 10px" }}> | |
| {runCode ? ( | |
| <> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}> | |
| <span style={{ fontSize: "0.62rem", color: "#4a4a78", fontFamily: "monospace" }}> | |
| {runCode.filename} | |
| </span> | |
| <button | |
| onClick={onGoToRun} | |
| style={{ | |
| display: "flex", alignItems: "center", gap: 3, | |
| background: "rgba(34,197,94,0.12)", border: "1px solid rgba(34,197,94,0.25)", | |
| borderRadius: 8, padding: "2px 8px", cursor: "pointer", | |
| color: "#4ade80", fontSize: "0.65rem", fontWeight: 600, | |
| transition: "background 0.15s", | |
| }} | |
| onMouseEnter={e => { e.currentTarget.style.background = "rgba(34,197,94,0.22)"; }} | |
| onMouseLeave={e => { e.currentTarget.style.background = "rgba(34,197,94,0.12)"; }} | |
| > | |
| <Play size={9} /> | |
| Esegui | |
| </button> | |
| </div> | |
| <pre style={{ | |
| margin: 0, fontSize: "0.68rem", color: "#9ca3af", | |
| fontFamily: '"JetBrains Mono", ui-monospace, monospace', | |
| lineHeight: 1.5, | |
| whiteSpace: "pre-wrap", wordBreak: "break-all", | |
| maxHeight: 200, overflow: "auto", | |
| }}> | |
| {runCode.code.slice(0, 800)} | |
| {runCode.code.length > 800 && "\n… (troncato)"} | |
| </pre> | |
| </> | |
| ) : ( | |
| <p style={{ fontSize: "0.68rem", color: "#3a3a6a", lineHeight: 1.5 }}> | |
| Nessun codice in esecuzione. L'agente popolerà questo pannello quando eseguirà codice. | |
| </p> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // ─── Desktop shelf (embedded in PanelGroup) ─────────────────────────────────── | |
| export const WorkspaceOutputShelf = memo(function WorkspaceOutputShelf({ | |
| onGoToTerminal, | |
| onGoToRun, | |
| }: { | |
| onGoToTerminal: () => void; | |
| onGoToRun: () => void; | |
| }) { | |
| const [tab, setTab] = useState<ShelfTab>("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<VfsTsError[]>([]); | |
| 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 ( | |
| <div style={{ | |
| display: "flex", flexDirection: "column", | |
| background: "rgba(4,5,16,0.97)", | |
| borderTop: "1px solid rgba(99,102,241,0.18)", | |
| overflow: "hidden", height: "100%", | |
| }}> | |
| {/* Header + tabs */} | |
| <div style={{ | |
| display: "flex", alignItems: "center", gap: 0, | |
| borderBottom: "1px solid rgba(99,102,241,0.10)", | |
| flexShrink: 0, height: 30, | |
| }}> | |
| {([ | |
| { 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 => ( | |
| <button key={t.id} onClick={() => setTab(t.id)} style={{ | |
| display: "flex", alignItems: "center", gap: 4, | |
| padding: "0 12px", height: "100%", | |
| background: tab === t.id ? "rgba(99,102,241,0.08)" : "none", | |
| border: "none", borderBottom: tab === t.id ? "2px solid #818cf8" : "2px solid transparent", | |
| cursor: "pointer", | |
| color: tab === t.id ? "#818cf8" : "#3a3a6a", | |
| fontSize: "0.6rem", fontWeight: 700, letterSpacing: "0.06em", | |
| transition: "color 0.15s, border-color 0.15s", | |
| }}> | |
| {t.id === "activity" ? <Zap size={10} /> : <Play size={10} />} | |
| {t.label} | |
| {t.badge > 0 && ( | |
| <span style={{ | |
| background: t.badgeColor, | |
| color: "#fff", fontSize: "0.55rem", fontWeight: 700, | |
| borderRadius: 4, padding: "0 3px", minWidth: 14, textAlign: "center", | |
| }}> | |
| {t.badge > 9 ? "9+" : t.badge} | |
| </span> | |
| )} | |
| </button> | |
| ))} | |
| {/* Right spacer + links */} | |
| <div style={{ flex: 1 }} /> | |
| <button onClick={onGoToTerminal} title="Vai al terminale" | |
| style={{ background: "none", border: "none", cursor: "pointer", | |
| color: "#3a3a6a", padding: "0 8px", height: "100%", display: "flex", | |
| alignItems: "center", transition: "color 0.15s" }} | |
| onMouseEnter={e => { e.currentTarget.style.color = "#818cf8"; }} | |
| onMouseLeave={e => { e.currentTarget.style.color = "#3a3a6a"; }} | |
| > | |
| <Terminal size={12} /> | |
| </button> | |
| <button onClick={onGoToRun} title="Vai a Esegui" | |
| style={{ background: "none", border: "none", cursor: "pointer", | |
| color: "#3a3a6a", padding: "0 8px", height: "100%", display: "flex", | |
| alignItems: "center", transition: "color 0.15s" }} | |
| onMouseEnter={e => { e.currentTarget.style.color = "#22c55e"; }} | |
| onMouseLeave={e => { e.currentTarget.style.color = "#3a3a6a"; }} | |
| > | |
| <Play size={12} /> | |
| </button> | |
| </div> | |
| {/* Content — S614: passa activeTask/isStreaming come props, evita triple subscription */} | |
| <ShelfContent | |
| tab={tab} | |
| onGoToTerminal={onGoToTerminal} | |
| onGoToRun={onGoToRun} | |
| activeTask={activeTask} | |
| agentLiveStatus={agentLiveStatus} | |
| isStreaming={isStreaming} | |
| tsErrors={tsErrors} | |
| tsChecking={tsChecking} | |
| onRepairTs={_onRepairTs} | |
| /> | |
| </div> | |
| ); | |
| }); | |
| // ─── Mobile collapseable shelf ──────────────────────────────────────────────── | |
| export const MobileOutputShelf = memo(function MobileOutputShelf({ | |
| onGoToTerminal, | |
| onGoToRun, | |
| }: { | |
| onGoToTerminal: () => void; | |
| onGoToRun: () => void; | |
| }) { | |
| const [open, setOpen] = useState(false); | |
| const [tab, setTab] = useState<ShelfTab>("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<VfsTsError[]>([]); | |
| 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 ( | |
| <div style={{ | |
| borderTop: "1px solid rgba(99,102,241,0.18)", | |
| background: "rgba(4,5,16,0.97)", | |
| flexShrink: 0, | |
| transition: "max-height 0.25s cubic-bezier(0.4,0,0.2,1)", | |
| maxHeight: open ? "40vh" : "30px", | |
| overflow: "hidden", | |
| display: "flex", flexDirection: "column", | |
| }}> | |
| {/* Toggle bar */} | |
| <button | |
| onClick={() => setOpen(v => !v)} | |
| style={{ | |
| display: "flex", alignItems: "center", gap: 6, | |
| padding: "0 12px", height: 30, | |
| background: "none", border: "none", cursor: "pointer", | |
| color: "#4a4a78", fontSize: "0.62rem", fontWeight: 700, | |
| letterSpacing: "0.06em", width: "100%", textAlign: "left", | |
| flexShrink: 0, | |
| }} | |
| > | |
| <Zap size={10} /> | |
| ATTIVITÀ | |
| {actCount > 0 && ( | |
| <span style={{ | |
| background: isStreaming ? "#22c55e" : "#3b82f6", | |
| color: "#fff", fontSize: "0.55rem", fontWeight: 700, | |
| borderRadius: 4, padding: "0 3px", minWidth: 14, textAlign: "center", | |
| }}> | |
| {actCount > 9 ? "9+" : actCount} | |
| </span> | |
| )} | |
| <span style={{ flex: 1 }} /> | |
| {open ? <ChevronDown size={12} /> : <ChevronUp size={12} />} | |
| </button> | |
| {/* Shelf content when open */} | |
| {open && ( | |
| <div style={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}> | |
| {/* Tabs */} | |
| <div style={{ | |
| display: "flex", borderBottom: "1px solid rgba(99,102,241,0.10)", | |
| flexShrink: 0, | |
| }}> | |
| {(["activity", "code", "errors"] as ShelfTab[]).map(t => ( | |
| <button key={t} onClick={() => setTab(t)} style={{ | |
| padding: "4px 12px", background: "none", | |
| border: "none", borderBottom: tab === t ? "2px solid #818cf8" : "2px solid transparent", | |
| cursor: "pointer", color: tab === t ? "#818cf8" : "#3a3a6a", | |
| fontSize: "0.6rem", fontWeight: 700, letterSpacing: "0.05em", | |
| display: "flex", alignItems: "center", gap: 4, | |
| }}> | |
| {t === "activity" ? "ATTIVITÀ" : t === "code" ? "CODICE" : "ERRORI"} | |
| {t === "errors" && tsErrors.length > 0 && ( | |
| <span style={{ | |
| background: tsErrors.some(e=>e.sev==="error") ? "#ef4444" : "#f59e0b", | |
| color: "#fff", fontSize: "0.52rem", fontWeight: 700, | |
| borderRadius: 4, padding: "0 3px", minWidth: 14, textAlign: "center", | |
| }}> | |
| {tsErrors.length > 9 ? "9+" : tsErrors.length} | |
| </span> | |
| )} | |
| </button> | |
| ))} | |
| <div style={{ flex: 1 }} /> | |
| <button onClick={() => setOpen(false)} style={{ | |
| background: "none", border: "none", cursor: "pointer", | |
| color: "#3a3a6a", padding: "0 10px", | |
| }}> | |
| <X size={12} /> | |
| </button> | |
| </div> | |
| <ShelfContent | |
| tab={tab} | |
| onGoToTerminal={() => { onGoToTerminal(); setOpen(false); }} | |
| onGoToRun={() => { onGoToRun(); setOpen(false); }} | |
| activeTask={activeTask} | |
| agentLiveStatus={agentLiveStatus} | |
| isStreaming={isStreaming} | |
| tsErrors={tsErrors} | |
| tsChecking={tsCheckingM} | |
| onRepairTs={_onRepairTsM} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }); | |