/** * useWorkspacePanel.ts — S-WP1: Pannelli UI, tastiera, responsive, badge * * Estratto da MainWorkspace.tsx (S-WP1): * - Tutti i subscriber UIStore (tab, sidebar, panel toggles, hasToken, badges…) * - tabsVisited (lazy-mount tracking) * - Effetto responsive (isMobile resize listener) * - Effetto keyboard shortcuts (Escape, Cmd+K, Ctrl+P, Shift+H/S/L/P/A) * - Effetto visualViewport (--vvh + scroll-jump prevention) * - Effetto memory badge pulse * - Effetto refresh badge counts when panels close * - Boot effect: hasToken * * Rimane in MainWorkspace: ConversationStore, WorkspaceStore, boot side-effects * (prewarm, hydrate, agentLoop, GH, SSE), selfDiag, VFS, JSX. */ import { useState, useRef, useEffect } from "react"; import { useIsLandscape } from "@/hooks/use-mobile"; import { useUIStore, selectAlertCount } from "@/store/uiStore"; import { useConversationStore } from "@/store/conversationStore"; import { useWorkspaceStore } from "@/store/workspaceStore"; import { agentMemory } from "@/lib/agentMemory"; import { getConnectors } from "@/lib/connectors"; import { getAnyToken } from "@/lib/messageBuilders"; import { onTokenInvalid } from "@/lib/providerStatusEmitter"; // TOKEN-FIX-5: 401 → reset hasToken // ───────────────────────────────────────────────────────────────────────────── interface UseWorkspacePanelOptions { /** AbortController ref from useConversationManager — Escape key aborts stream */ abortRef: React.RefObject; } export function useWorkspacePanel({ abortRef }: UseWorkspacePanelOptions) { // ── UIStore — tab ───────────────────────────────────────────────────────── const tab = useUIStore(s => s.activeTab); const setTab = useUIStore(s => s.setActiveTab); // ── Lazy mount: tab visitati — FileEditor e TerminalPanel montati solo dopo primo accesso const [tabsVisited, setTabsVisited] = useState>(new Set(["chat"])); useEffect(() => { setTabsVisited(prev => { const s = new Set([...prev]); s.add(tab); return s; }); }, [tab]); // ── UIStore — sidebar + settings ───────────────────────────────────────── const sidebarOpen = useUIStore(s => s.sidebarOpen); const setSidebarOpen = useUIStore(s => s.setSidebarOpen); const toggleSidebar = useUIStore(s => s.toggleSidebar); const showSettings = useUIStore(s => s.showSettings); const setShowSettings = useUIStore(s => s.setShowSettings); // ── UIStore — panel toggles ─────────────────────────────────────────────── const toggleTaskHistory = useUIStore(s => s.toggleTaskHistory); const toggleSnapshotPanel = useUIStore(s => s.toggleSnapshotPanel); const toggleRuntimeConsole = useUIStore(s => s.toggleRuntimeConsole); const togglePythonVars = useUIStore(s => s.togglePythonVars); const showConnectors = useUIStore(s => s.showConnectors); const showMemory = useUIStore(s => s.showMemory); const toggleAdminTasks = useUIStore(s => s.toggleAdminTasks); // S370 const toggleQuickOpen = useUIStore(s => s.toggleQuickOpen); // S617 // ── UIStore — layout ────────────────────────────────────────────────────── const workspaceMode = useUIStore(s => s.workspaceMode); const chatPanelOpen = useUIStore(s => s.chatPanelOpen); const isMobile = useUIStore(s => s.isMobile); const isLandscape = useIsLandscape(); // S464: landscape mobile layout // ── UIStore — auth + badges ─────────────────────────────────────────────── const hasToken = useUIStore(s => s.hasToken); const setHasToken = useUIStore(s => s.setHasToken); const setMemPulse = useUIStore(s => s.setMemPulse); const setMemoryCount = useUIStore(s => s.setMemoryCount); const setConnectorCount = useUIStore(s => s.setConnectorCount); const systemAlerts = useUIStore(s => s.systemAlerts); const alertCount = useUIStore(selectAlertCount); const addAlert = useUIStore(s => s.addAlert); // ── Internal ref: memory badge count tracking ───────────────────────────── const prevMemCountRef = useRef(agentMemory.list().length); // ── Boot: hasToken ──────────────────────────────────────────────────────── useEffect(() => { setHasToken(!!getAnyToken()); }, [setHasToken]); // ── TOKEN-FIX-5: 401 token-invalid → reset hasToken → TokenSetup schermata ── // emitTokenInvalid() è chiamato da providerFallbackChain/providerTryCore/agentLoop // quando un provider risponde 401 (token scaduto o non valido). Prima non aveva // subscriber → 401 silenzioso, utente vedeva solo "Tutti i provider non hanno risposto" // senza capire il motivo. Ora resetta hasToken → mostra TokenSetup per aggiornare. useEffect(() => onTokenInvalid(() => { setHasToken(false); }), [setHasToken]); // ── Keyboard shortcuts ──────────────────────────────────────────────────── useEffect(() => { const h = (e: KeyboardEvent) => { // Escape: ferma streaming if (e.key === "Escape") { if (abortRef.current) { e.preventDefault(); abortRef.current.abort(); } return; } // Cmd+K (Mac) / Ctrl+K (Win/Linux): nuova conversazione / Command Palette if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); useConversationStore.getState().newConv(); return; } // S626: Ctrl+Shift+T / Cmd+Shift+T — riapri ultimo tab chiuso (VS Code standard) if (e.key === "t" && (e.metaKey || e.ctrlKey) && e.shiftKey) { e.preventDefault(); useWorkspaceStore.getState().reopenLastClosed(); return; } // S617/S650: Ctrl+P / Cmd+P — Quick Open palette if (e.key === "p" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { e.preventDefault(); toggleQuickOpen(); return; } if (!e.shiftKey) return; const tag = (e.target as HTMLElement)?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (e.target as HTMLElement)?.isContentEditable) return; if (e.key === "H") { e.preventDefault(); toggleTaskHistory(); } if (e.key === "S") { e.preventDefault(); toggleSnapshotPanel(); } if (e.key === "L") { e.preventDefault(); toggleRuntimeConsole(); } if (e.key === "P") { e.preventDefault(); togglePythonVars(); } if (e.key === "A") { e.preventDefault(); toggleAdminTasks(); } // S370 }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [abortRef, toggleTaskHistory, toggleSnapshotPanel, toggleRuntimeConsole, togglePythonVars, toggleAdminTasks, toggleQuickOpen]); // ── visualViewport: --vvh + blocca scroll-jump ──────────────────────────── useEffect(() => { const vv = (window as Window & { visualViewport?: VisualViewport }).visualViewport; if (!vv) return; const onWinScroll = () => { if (window.scrollY !== 0) window.scrollTo(0, 0); }; window.addEventListener("scroll", onWinScroll, { passive: true }); return () => { window.removeEventListener("scroll", onWinScroll); }; }, []); // ── Memory badge pulse ──────────────────────────────────────────────────── useEffect(() => { const unsub = agentMemory.subscribe(() => { const newCount = agentMemory.list().length; if (newCount > prevMemCountRef.current) setMemPulse(true); prevMemCountRef.current = newCount; }); return unsub; }, [setMemPulse]); // ── Refresh badge counts when panels close ──────────────────────────────── useEffect(() => { if (!showMemory) setMemoryCount(agentMemory.list().length); if (!showConnectors) setConnectorCount(getConnectors().length); }, [showMemory, showConnectors, setMemoryCount, setConnectorCount]); return { tab, setTab, tabsVisited, sidebarOpen, setSidebarOpen, toggleSidebar, showSettings, setShowSettings, toggleTaskHistory, toggleSnapshotPanel, toggleRuntimeConsole, togglePythonVars, toggleAdminTasks, toggleQuickOpen, showConnectors, showMemory, workspaceMode, chatPanelOpen, isMobile, isLandscape, hasToken, setHasToken, systemAlerts, alertCount, addAlert, setMemPulse, setMemoryCount, setConnectorCount, }; }