"use client"; import { Menu, X } from "lucide-react"; import Image from "next/image"; import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react"; import { toast } from "sonner"; import { ActivityView } from "@/components/sf/activity-view"; import { ChatMode } from "@/components/sf/chat-mode"; import { CommandSurface } from "@/components/sf/command-surface"; import { Dashboard } from "@/components/sf/dashboard"; import { DualTerminal } from "@/components/sf/dual-terminal"; import { ErrorBoundary } from "@/components/sf/error-boundary"; import { FilesView } from "@/components/sf/files-view"; import { FocusedPanel } from "@/components/sf/focused-panel"; import { OnboardingGate } from "@/components/sf/onboarding-gate"; import { ProjectSelectionGate, ProjectsPanel, } from "@/components/sf/projects-view"; import { Roadmap } from "@/components/sf/roadmap"; import { ScopeBadge } from "@/components/sf/scope-badge"; import { ShellTerminal } from "@/components/sf/shell-terminal"; import { CollapsedMilestoneSidebar, MilestoneExplorer, Sidebar, } from "@/components/sf/sidebar"; import { StatusBar } from "@/components/sf/status-bar"; import { UpdateBanner } from "@/components/sf/update-banner"; import { VisualizerView } from "@/components/sf/visualizer-view"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { getAuthToken } from "@/lib/auth"; import { DevOverridesProvider } from "@/lib/dev-overrides"; import { ProjectStoreManagerProvider, useProjectStoreManager, } from "@/lib/project-store-manager"; import { getCurrentScopeLabel, getProjectDisplayName, getStatusPresentation, getVisibleWorkspaceError, SFWorkspaceProvider, useSFWorkspaceActions, useSFWorkspaceState, } from "@/lib/sf-workspace-store"; import { cn } from "@/lib/utils"; const KNOWN_VIEWS = new Set([ "dashboard", "power", "chat", "roadmap", "files", "activity", "visualize", ]); function viewStorageKey(projectCwd: string): string { return `sf-active-view:${projectCwd}`; } function WorkspaceChrome() { const [activeView, setActiveView] = useState("dashboard"); const [isTerminalExpanded, setIsTerminalExpanded] = useState(false); const [terminalHeight, setTerminalHeight] = useState(300); const [terminalDragActive, setTerminalDragActive] = useState(false); const isDraggingTerminal = useRef(false); const didDragTerminal = useRef(false); const dragStartY = useRef(0); const dragStartHeight = useRef(0); const [sidebarWidth, setSidebarWidth] = useState(256); const isDraggingSidebar = useRef(false); const dragStartX = useRef(0); const dragStartWidth = useRef(0); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [viewRestored, setViewRestored] = useState(false); const [projectsPanelOpen, setProjectsPanelOpen] = useState(false); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [mobileMilestoneOpen, setMobileMilestoneOpen] = useState(false); const workspace = useSFWorkspaceState(); const { refreshBoot } = useSFWorkspaceActions(); const status = getStatusPresentation(workspace); const projectPath = workspace.boot?.project.cwd; const projectLabel = getProjectDisplayName(projectPath); const titleOverride = workspace.titleOverride?.trim() || null; const scopeLabel = getCurrentScopeLabel(workspace.boot?.workspace); const visibleError = getVisibleWorkspaceError(workspace); // Restore persisted view once boot provides projectCwd useEffect(() => { if (viewRestored || !projectPath) return; const restoreTimer = window.setTimeout(() => { try { const stored = sessionStorage.getItem(viewStorageKey(projectPath)); if (stored && KNOWN_VIEWS.has(stored)) { setActiveView(stored); } } catch { // sessionStorage may be unavailable (e.g. SSR, iframe sandbox) } setViewRestored(true); }, 0); return () => window.clearTimeout(restoreTimer); }, [projectPath, viewRestored]); // Reset viewRestored when projectPath changes so the restore effect can // fire for the newly-selected project (fixes #2711: tab reset on switch). const prevProjectPath = useRef(projectPath); useEffect(() => { if (prevProjectPath.current !== projectPath) { prevProjectPath.current = projectPath; setViewRestored(false); } }, [projectPath]); // Persist view changes to sessionStorage useEffect(() => { if (!projectPath) return; try { sessionStorage.setItem(viewStorageKey(projectPath), activeView); } catch { // sessionStorage may be unavailable } }, [activeView, projectPath]); // Restore sidebar collapsed state from localStorage useEffect(() => { const restoreTimer = window.setTimeout(() => { try { const stored = localStorage.getItem("sf-sidebar-collapsed"); if (stored === "true") setSidebarCollapsed(true); } catch { // localStorage may be unavailable } }, 0); return () => window.clearTimeout(restoreTimer); }, []); // Persist sidebar collapsed state useEffect(() => { try { localStorage.setItem("sf-sidebar-collapsed", String(sidebarCollapsed)); } catch { // localStorage may be unavailable } }, [sidebarCollapsed]); useEffect(() => { if (typeof document === "undefined") return; const base = projectLabel ? `SF - ${projectLabel}` : "SF"; document.title = titleOverride ? `${titleOverride} · ${base}` : base; }, [titleOverride, projectLabel]); // Close mobile nav on view change const handleViewChange = useCallback((view: string) => { setActiveView(view); setMobileNavOpen(false); }, []); // Listen for cross-component file navigation events (e.g. sidebar task clicks) useEffect(() => { const handler = () => { setActiveView("files"); }; window.addEventListener("sf:open-file", handler); return () => window.removeEventListener("sf:open-file", handler); }, []); // Listen for cross-component view navigation events (e.g. /sf visualize dispatch) useEffect(() => { const handler = (e: CustomEvent<{ view: string }>) => { if (KNOWN_VIEWS.has(e.detail.view)) { handleViewChange(e.detail.view); } }; window.addEventListener("sf:navigate-view", handler as EventListener); return () => window.removeEventListener("sf:navigate-view", handler as EventListener); }, [handleViewChange]); // Listen for projects panel toggle (sidebar icon, or programmatic) useEffect(() => { const handler = () => setProjectsPanelOpen(true); window.addEventListener("sf:open-projects", handler); return () => window.removeEventListener("sf:open-projects", handler); }, []); // Terminal + sidebar panel drag-to-resize useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (isDraggingTerminal.current) { didDragTerminal.current = true; const delta = dragStartY.current - e.clientY; const newHeight = Math.max( 150, Math.min(600, dragStartHeight.current + delta), ); setTerminalHeight(newHeight); } if (isDraggingSidebar.current) { const delta = dragStartX.current - e.clientX; const newWidth = Math.max( 180, Math.min(480, dragStartWidth.current + delta), ); setSidebarWidth(newWidth); } }; const handleMouseUp = () => { isDraggingTerminal.current = false; isDraggingSidebar.current = false; setTerminalDragActive(false); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, []); const handleTerminalDragStart = useCallback( (e: React.MouseEvent) => { isDraggingTerminal.current = true; setTerminalDragActive(true); dragStartY.current = e.clientY; dragStartHeight.current = terminalHeight; document.body.style.cursor = "row-resize"; document.body.style.userSelect = "none"; }, [terminalHeight], ); const handleSidebarDragStart = useCallback( (e: React.MouseEvent) => { isDraggingSidebar.current = true; dragStartX.current = e.clientX; dragStartWidth.current = sidebarWidth; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }, [sidebarWidth], ); const retryDisabled = !!workspace.commandInFlight || workspace.onboardingRequestState !== "idle"; const isConnecting = workspace.bootStatus === "idle" || workspace.bootStatus === "loading"; // Persistent loading toast — dismissed the moment boot completes useEffect(() => { if (!isConnecting) return; const id = toast.loading("Connecting to workspace…", { description: "Establishing the live bridge session", duration: Infinity, }); return () => { toast.dismiss(id); }; }, [isConnecting]); // Detect project welcome state — hide chrome for v1-legacy, brownfield, blank projects const detection = workspace.boot?.projectDetection; const isWelcomeState = !isConnecting && activeView === "dashboard" && detection != null && detection.kind !== "active-sf" && detection.kind !== "empty-sf"; // --- Unauthenticated gate --- // Render a clear recovery screen before any workspace chrome is mounted so // users who open a manually-typed URL (no #token= fragment) get actionable // guidance instead of a cascade of 401 errors. if (workspace.bootStatus === "unauthenticated") { return (
This workspace requires an auth token. Copy the full URL from your
terminal (including the{" "}
#token=…
{" "}
part) or restart with{" "}
sf --web
.