/** * `ProjectsView` — the top-level project surface (L2 / L11). * * IDE layout (full remaining height): * * ┌───────────────────────────────────────────────────────┐ * │ PROJECT TABS [ alpha × ][ beta × ] [+] │ * ├───────────────┬───────────────────────────────────────┤ * │ SIDEBAR ≈320px│ MAIN — LayoutGrid (fills remaining) │ * │ [Proj][Agents]│ — or welcome screen when no active │ * │ [Tmpl][Git] │ │ * │ panel body │ │ * └───────────────┴───────────────────────────────────────┘ * * The "Projects" sidebar tab always hosts the create-project form and the * known-projects list, ensuring they are always in the DOM (required by tests). * * Pure presentation: all behaviour comes from {@link useProjects}. Styling via * `@/shared`; no inline styles, no `invoke()`. * * **Test-contract** — the following query hooks from `projects.test.tsx` are * always present in the DOM regardless of which sidebar tab is active: * - getByLabelText("project name") / ("project root") — form inputs * - getByRole("button", { name: "Create project" }) / "Refresh" / "Open" / "close " * - getByRole("tablist") + getAllByRole("tab") + aria-selected — project tabs * - getByText("No projects yet.") / "No open tabs." — empty states * - getAllByRole("listitem") — known-projects list * - getByRole("alert") — error display */ import { useState } from "react"; import type { LayoutInfo } from "@/domain"; import { LayoutGrid, LayoutTabs } from "@/features/layout"; import { AgentsPanel, ResumeProjectPanel } from "@/features/agents"; import { TemplatesPanel } from "@/features/templates"; import { SkillsPanel } from "@/features/skills"; import { MemoryPanel } from "@/features/memory"; import { EmbedderSettings } from "@/features/embedder"; import { PermissionsPanel } from "@/features/permissions"; import { GitPanel, GitGraphView } from "@/features/git"; import { Button, Input, Panel, Tabs, cn } from "@/shared"; import { useGateways } from "@/app/di"; import { ProjectContextPanel } from "./ProjectContextPanel"; import { useProjects } from "./useProjects"; type SidebarTab = | "projects" | "context" | "agents" | "templates" | "skills" | "permissions" | "memory" | "git"; const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [ { id: "projects", label: "Projects" }, { id: "context", label: "Context" }, { id: "agents", label: "Agents" }, { id: "templates", label: "Templates" }, { id: "skills", label: "Skills" }, { id: "permissions", label: "Perms" }, { id: "memory", label: "Memory" }, { id: "git", label: "Git" }, ]; export function ProjectsView() { const vm = useProjects(); const { system } = useGateways(); const [name, setName] = useState(""); const [root, setRoot] = useState(""); const [sidebarTab, setSidebarTab] = useState("projects"); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); // The active layout (id + kind), reported by LayoutTabs — the single source of // truth. `kind` decides whether the main area is the terminal grid or the git // graph view. const [activeLayout, setActiveLayout] = useState(null); const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null; const activeLayoutKind = activeLayout?.kind ?? "terminal"; const canCreate = name.trim().length > 0 && root.trim().length > 0 && !vm.busy; async function submit(e: React.FormEvent) { e.preventDefault(); if (!canCreate) return; await vm.createProject(name.trim(), root.trim()); setName(""); setRoot(""); } async function handleBrowse() { const picked = await system.pickFolder(); if (picked !== null) setRoot(picked); } return (
{/* ── Error alert ── */} {vm.error && (

{vm.error}

)} {/* ── Project tab bar ── */}
{vm.openTabs.length === 0 ? (

No open tabs.

) : ( ({ id: t.id, label: t.name }))} value={vm.activeTabId} onSelect={(id) => vm.activateTab(id)} onClose={(id) => void vm.closeTab(id)} className="flex-1" /> )}
{/* ── IDE body: sidebar + main ── */}
{/* ── Collapsed sidebar: a thin rail with an expand button ── */} {sidebarCollapsed && ( )} {/* ── Sidebar ── */} {/* ── Main: terminal grid or git graph (fills remaining height) ── */}
{active ? ( <> {activeLayoutKind === "gitGraph" ? ( ) : ( )} ) : (

Select or create a project to get started.

)}
{/* ── Reopen resume panel (§15.2) ── Mounted per active project (keyed by id) so it re-pulls the resumable inventory on every open/switch; it renders nothing when empty. */} {active && ( )}
); }