/**
* `GitPanel` — feature component for the Git panel (L8).
*
* Shows the working-tree status grouped by Staged / Unstaged, a commit form,
* branch management with checkout, and the recent commit log.
*
* Pure presentation: all behaviour comes from {@link useGit}. Styled with
* `@/shared`; no inline styles.
*/
import { useState } from "react";
import { Button, Input, Panel, Spinner, cn } from "@/shared";
import { useGit } from "./useGit";
import { buildFileTree, type GitTreeNode } from "./gitTree";
/**
* Recursively renders a changed-files tree. Directories are static rows; file
* leaves carry the stage/unstage action (its `aria-label` uses the full path).
*/
function FileTree({
nodes,
depth,
action,
onAction,
busy,
}: {
nodes: GitTreeNode[];
depth: number;
action: "stage" | "unstage";
onAction: (path: string) => void;
busy: boolean;
}) {
return (
{nodes.map((node) =>
node.isDir ? (
-
▸
{node.name}
) : (
-
{node.name}
),
)}
);
}
export interface GitPanelProps {
/** The project whose git state to manage. */
projectId: string;
}
export function GitPanel({ projectId }: GitPanelProps) {
const vm = useGit(projectId);
const [commitMessage, setCommitMessage] = useState("");
const [newBranch, setNewBranch] = useState("");
const staged = vm.files.filter((f) => f.staged);
const unstaged = vm.files.filter((f) => !f.staged);
const canCommit = commitMessage.trim().length > 0 && staged.length > 0 && !vm.busy;
async function handleCommit(e: React.FormEvent) {
e.preventDefault();
if (!canCommit) return;
await vm.commit(commitMessage.trim());
setCommitMessage("");
}
async function handleCheckout(branch: string) {
await vm.checkout(branch);
}
async function handleNewBranch(e: React.FormEvent) {
e.preventDefault();
const b = newBranch.trim();
if (!b || vm.busy) return;
await vm.checkout(b);
setNewBranch("");
}
return (
{vm.error && (
{vm.error}
)}
{/* ── Changes ── */}
Changes
{vm.busy && }
{vm.files.length === 0 ? (
No changes.
) : (
{/* Staged — shown as a file tree. */}
{staged.length > 0 && (
Staged
void vm.unstage(p)}
busy={vm.busy}
/>
)}
{/* Unstaged — shown as a file tree. */}
{unstaged.length > 0 && (
Unstaged
void vm.stage(p)}
busy={vm.busy}
/>
)}
)}
{/* ── Commit ── */}
{/* ── Branches ── */}
Branches
Current:{" "}
{vm.branches.current ?? "(none)"}
{vm.branches.branches.length > 0 && (
{vm.branches.branches.map((b) => {
const isCurrent = b === vm.branches.current;
return (
-
{b}
{!isCurrent && (
)}
);
})}
)}
{/* ── Log ── */}
Log
{vm.log.length === 0 ? (
No commits yet.
) : (
{vm.log.map((c) => (
-
{c.hash.slice(0, 7)}
{c.summary}
))}
)}
);
}