feat: add main features

Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,303 @@
/**
* `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 <name>"
* - 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 } from "@/features/agents";
import { TemplatesPanel } from "@/features/templates";
import { GitPanel, GitGraphView } from "@/features/git";
import { Button, Input, Panel, Tabs, cn } from "@/shared";
import { useGateways } from "@/app/di";
import { useProjects } from "./useProjects";
type SidebarTab = "projects" | "agents" | "templates" | "git";
const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [
{ id: "projects", label: "Projects" },
{ id: "agents", label: "Agents" },
{ id: "templates", label: "Templates" },
{ 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<SidebarTab>("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<LayoutInfo | null>(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 (
<div className="flex flex-1 flex-col overflow-hidden">
{/* ── Error alert ── */}
{vm.error && (
<p
role="alert"
className="mx-4 mt-2 shrink-0 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger"
>
{vm.error}
</p>
)}
{/* ── Project tab bar ── */}
<div className="flex shrink-0 items-center gap-1 border-b border-border bg-surface px-2 py-1">
{vm.openTabs.length === 0 ? (
<p className="px-2 text-sm text-muted">No open tabs.</p>
) : (
<Tabs
items={vm.openTabs.map((t) => ({ id: t.id, label: t.name }))}
value={vm.activeTabId}
onSelect={(id) => vm.activateTab(id)}
onClose={(id) => void vm.closeTab(id)}
className="flex-1"
/>
)}
</div>
{/* ── IDE body: sidebar + main ── */}
<div className="flex flex-1 overflow-hidden">
{/* ── Collapsed sidebar: a thin rail with an expand button ── */}
{sidebarCollapsed && (
<aside className="flex w-9 shrink-0 flex-col items-center border-r border-border bg-surface py-1">
<button
type="button"
aria-label="expand sidebar"
title="Expand sidebar"
onClick={() => setSidebarCollapsed(false)}
className="flex h-7 w-7 items-center justify-center rounded text-muted hover:bg-raised hover:text-content"
>
»
</button>
</aside>
)}
{/* ── Sidebar ── */}
<aside
className={cn(
"flex w-80 shrink-0 flex-col border-r border-border bg-surface",
sidebarCollapsed && "hidden",
)}
>
{/* Sidebar tab strip (no role="tablist" to avoid collision with project tabs) */}
<div className="flex shrink-0 items-stretch border-b border-border">
{SIDEBAR_TABS.map((t) => (
<button
key={t.id}
type="button"
aria-selected={sidebarTab === t.id}
onClick={() => setSidebarTab(t.id)}
className={cn(
"flex-1 px-2 py-2 text-xs font-medium transition-colors",
sidebarTab === t.id
? "border-b-2 border-primary text-content"
: "text-muted hover:text-content",
)}
>
{t.label}
</button>
))}
<button
type="button"
aria-label="collapse sidebar"
title="Collapse sidebar"
onClick={() => setSidebarCollapsed(true)}
className="shrink-0 px-2 text-muted hover:text-content"
>
«
</button>
</div>
{/* Sidebar panel body */}
<div className="flex-1 overflow-auto p-3">
{/* Projects panel */}
{sidebarTab === "projects" && (
<div className="flex flex-col gap-4">
{/* Create form */}
<form
onSubmit={submit}
className="flex flex-col gap-2"
>
<h3 className="text-xs font-semibold uppercase tracking-wide text-faint">
New project
</h3>
<Input
aria-label="project name"
placeholder="Project name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<div className="flex gap-2">
<Input
aria-label="project root"
placeholder="/absolute/project/root"
value={root}
onChange={(e) => setRoot(e.target.value)}
className="flex-1"
/>
<Button
type="button"
aria-label="browse project folder"
onClick={() => void handleBrowse()}
disabled={vm.busy}
>
Browse
</Button>
</div>
<div className="flex gap-2">
<Button
type="submit"
variant="primary"
disabled={!canCreate}
className="flex-1"
>
Create project
</Button>
<Button
type="button"
onClick={() => void vm.refresh()}
disabled={vm.busy}
>
Refresh
</Button>
</div>
</form>
{/* Known projects list */}
<Panel title="Known projects">
{vm.projects.length === 0 ? (
<p className="text-sm text-muted">No projects yet.</p>
) : (
<ul className="flex flex-col divide-y divide-border">
{vm.projects.map((p) => (
<li
key={p.id}
className="flex items-center justify-between gap-2 py-2 first:pt-0 last:pb-0"
>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="font-medium text-content">{p.name}</span>
<code className="truncate text-xs text-muted">{p.root}</code>
</span>
<Button
size="sm"
onClick={() => void vm.openProject(p.id)}
>
Open
</Button>
</li>
))}
</ul>
)}
</Panel>
</div>
)}
{/* Agents panel — only rendered when active project exists */}
{sidebarTab === "agents" && active && (
<AgentsPanel projectId={active.id} projectRoot={active.root} />
)}
{sidebarTab === "agents" && !active && (
<p className="text-sm text-muted">Open a project to manage agents.</p>
)}
{/* Templates panel */}
{sidebarTab === "templates" && active && (
<TemplatesPanel projectId={active.id} />
)}
{sidebarTab === "templates" && !active && (
<p className="text-sm text-muted">Open a project to manage templates.</p>
)}
{/* Git panel */}
{sidebarTab === "git" && active && (
<GitPanel projectId={active.id} />
)}
{sidebarTab === "git" && !active && (
<p className="text-sm text-muted">Open a project to manage git.</p>
)}
</div>
</aside>
{/* ── Main: terminal grid or git graph (fills remaining height) ── */}
<main className="flex flex-1 flex-col overflow-hidden">
{active ? (
<>
<LayoutTabs
projectId={active.id}
onActiveLayoutChange={setActiveLayout}
/>
{activeLayoutKind === "gitGraph" ? (
<GitGraphView
key={`${active.id}-${activeLayout?.id ?? "default"}-graph`}
projectId={active.id}
/>
) : (
<LayoutGrid
key={`${active.id}-${activeLayout?.id ?? "default"}`}
projectId={active.id}
cwd={active.root}
layoutId={activeLayout?.id}
/>
)}
</>
) : (
<div className="flex flex-1 items-center justify-center text-muted">
<p className="text-sm">Select or create a project to get started.</p>
</div>
)}
</main>
</div>
</div>
);
}