Spaces:
Sleeping
Sleeping
| /** | |
| * DebugFixBanner.tsx — Live Auto-Debug Toast (S60) | |
| * | |
| * Mostra all'utente cosa sta facendo il loop agentLoop → debugLoop in tempo reale. | |
| * Appare automaticamente quando un tool run_code / execute_shell fallisce e | |
| * l'agente tenta di auto-ripararlo. | |
| * | |
| * Stati: | |
| * fixing → pillola arancio animata "🔧 Auto-debug in corso…" | |
| * fixed → pillola verde "✅ Codice corretto automaticamente!" (sparisce dopo 4s) | |
| * failed → pillola rossa "❌ Auto-debug: impossibile correggere" (sparisce dopo 3s) | |
| * | |
| * iPhone-safe: solo CSS transitions, zero Worker/SharedArrayBuffer. | |
| * Non mostra StreamChunk — filtra solo live_debug_* events. | |
| */ | |
| import { memo, useState, useEffect, useCallback, useRef } from "react"; | |
| import type * as React from "react"; // FIX11: React namespace types | |
| import { Wrench, CheckCircle, XCircle } from "lucide-react"; | |
| import { useAnyEvent } from "@/core/events/useEventStream"; | |
| import type { RuntimeEvent } from "@/core/events/EventTypes"; | |
| import { Z_INDEX } from "@/lib/zindex"; | |
| // ─── Tipi stato ─────────────────────────────────────────────────────────────── | |
| type DebugState = | |
| | { phase: "idle" } | |
| | { phase: "fixing"; taskId: string; attempt: number; toolName: string } | |
| | { phase: "fixed"; taskId: string; attempts: number; snippet?: string } | |
| | { phase: "failed"; taskId: string; reason: string }; | |
| // ─── Colori e icone per fase ────────────────────────────────────────────────── | |
| function stateStyle(phase: DebugState["phase"]): React.CSSProperties { | |
| if (phase === "fixing") return { | |
| background: "linear-gradient(135deg, rgba(251,146,60,0.18), rgba(245,158,11,0.12))", | |
| border: "1px solid rgba(251,146,60,0.4)", | |
| color: "#fb923c", | |
| }; | |
| if (phase === "fixed") return { | |
| background: "linear-gradient(135deg, rgba(74,222,128,0.18), rgba(59,130,246,0.12))", | |
| border: "1px solid rgba(74,222,128,0.4)", | |
| color: "#4ade80", | |
| }; | |
| return { | |
| background: "linear-gradient(135deg, rgba(248,113,113,0.18), rgba(239,68,68,0.10))", | |
| border: "1px solid rgba(248,113,113,0.35)", | |
| color: "#f87171", | |
| }; | |
| } | |
| // ─── Pillola principale ─────────────────────────────────────────────────────── | |
| function DebugPill({ state }: { state: Exclude<DebugState, { phase: "idle" }> }) { | |
| const { phase } = state; | |
| const style = stateStyle(phase); | |
| const icon = | |
| phase === "fixing" ? <Wrench size={13} style={{ flexShrink: 0, animation: "debug-spin 1.6s linear infinite" }} /> : | |
| phase === "fixed" ? <CheckCircle size={13} style={{ flexShrink: 0 }} /> : | |
| <XCircle size={13} style={{ flexShrink: 0 }} />; | |
| const label = | |
| phase === "fixing" | |
| ? `Auto-debug ${state.toolName}: tentativo ${state.attempt}…` | |
| : phase === "fixed" | |
| ? `Codice corretto in ${state.attempts} tentativ${state.attempts === 1 ? "o" : "i"}` | |
| : `Auto-debug non riuscito`; | |
| const sub = | |
| phase === "fixed" && (state as { phase: "fixed"; snippet?: string }).snippet | |
| ? (state as { phase: "fixed"; snippet?: string }).snippet!.slice(0, 80) | |
| : phase === "failed" | |
| ? (state as { phase: "failed"; reason: string }).reason | |
| : ""; | |
| return ( | |
| <div style={{ | |
| display: "flex", alignItems: "flex-start", gap: 8, | |
| padding: "8px 14px", borderRadius: 12, | |
| boxShadow: "0 4px 20px rgba(0,0,0,0.4)", | |
| maxWidth: 360, | |
| ...style, | |
| }}> | |
| {/* icona */} | |
| <span style={{ paddingTop: 1 }}>{icon}</span> | |
| {/* testo */} | |
| <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}> | |
| <span style={{ | |
| fontSize: "0.72rem", fontWeight: 700, letterSpacing: "0.02em", | |
| whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", | |
| }}> | |
| {label} | |
| </span> | |
| {sub && ( | |
| <span style={{ | |
| fontSize: "0.63rem", opacity: 0.75, | |
| overflow: "hidden", textOverflow: "ellipsis", | |
| whiteSpace: "nowrap", fontFamily: "ui-monospace,'SF Mono',monospace", | |
| }}> | |
| {sub} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ─── Banner principale ──────────────────────────────────────────────────────── | |
| const DebugFixBanner = memo(function DebugFixBanner() { | |
| const [state, setState] = useState<DebugState>({ phase: "idle" }); | |
| const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| const clearHide = () => { | |
| if (hideTimer.current !== null) { clearTimeout(hideTimer.current); hideTimer.current = null; } | |
| }; | |
| const scheduleHide = useCallback((ms: number) => { | |
| clearHide(); | |
| hideTimer.current = setTimeout(() => setState({ phase: "idle" }), ms); | |
| }, []); | |
| useEffect(() => () => clearHide(), []); | |
| useAnyEvent(useCallback((ev: RuntimeEvent) => { | |
| if (ev.kind === "live_debug_start") { | |
| clearHide(); | |
| setState({ phase: "fixing", taskId: ev.taskId, attempt: ev.attempt, toolName: ev.toolName }); | |
| } else if (ev.kind === "live_debug_fix") { | |
| setState(prev => | |
| prev.phase === "fixing" | |
| ? { ...prev, attempt: ev.attempt } | |
| : prev | |
| ); | |
| } else if (ev.kind === "live_debug_success") { | |
| setState({ phase: "fixed", taskId: ev.taskId, attempts: ev.attempts }); | |
| scheduleHide(4500); | |
| } else if (ev.kind === "live_debug_fail") { | |
| setState({ phase: "failed", taskId: ev.taskId, reason: ev.reason }); | |
| scheduleHide(3000); | |
| } | |
| }, [scheduleHide])); | |
| if (state.phase === "idle") return null; | |
| return ( | |
| <> | |
| {/* Keyframe per l'icona rotante */} | |
| <style>{` | |
| @keyframes debug-spin { | |
| 0% { transform: rotate(0deg); } | |
| 25% { transform: rotate(-15deg); } | |
| 75% { transform: rotate(15deg); } | |
| 100% { transform: rotate(0deg); } | |
| } | |
| .debug-banner-enter { | |
| animation: debug-banner-in 0.25s ease-out forwards; | |
| } | |
| @keyframes debug-banner-in { | |
| from { opacity: 0; transform: translateY(8px) scale(0.96); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| `}</style> | |
| <div | |
| className="debug-banner-enter" | |
| style={{ | |
| position: "fixed", | |
| bottom: 72, | |
| left: "50%", | |
| transform: "translateX(-50%)", | |
| zIndex: Z_INDEX.SIDEBAR_PANEL + 5, | |
| pointerEvents: "none", | |
| }} | |
| > | |
| <DebugPill state={state} /> | |
| </div> | |
| </> | |
| ); | |
| } | |
| ); | |
| export default DebugFixBanner; | |