import { memo, useState, useCallback } from "react";
import type { EnvironmentId } from "@t3tools/contracts";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { ScrollArea } from "./ui/scroll-area";
import ChatMarkdown from "./ChatMarkdown";
import {
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
EllipsisIcon,
LoaderIcon,
PanelRightCloseIcon,
} from "lucide-react";
import { cn } from "~/lib/utils";
import type { ActivePlanState } from "../session-logic";
import type { LatestProposedPlanState } from "../session-logic";
import { formatTimestamp } from "../timestampFormat";
import {
proposedPlanTitle,
buildProposedPlanMarkdownFilename,
normalizePlanMarkdownForExport,
downloadPlanAsTextFile,
stripDisplayedPlanMarkdown,
} from "../proposedPlan";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu";
import { readEnvironmentApi } from "~/environmentApi";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
function stepStatusIcon(status: string): React.ReactNode {
if (status === "completed") {
return (
);
}
if (status === "inProgress") {
return (
);
}
return (
);
}
interface PlanSidebarProps {
activePlan: ActivePlanState | null;
activeProposedPlan: LatestProposedPlanState | null;
label?: string;
environmentId: EnvironmentId;
markdownCwd: string | undefined;
workspaceRoot: string | undefined;
timestampFormat: TimestampFormat;
mode?: "sheet" | "sidebar";
onClose: () => void;
}
const PlanSidebar = memo(function PlanSidebar({
activePlan,
activeProposedPlan,
label = "Plan",
environmentId,
markdownCwd,
workspaceRoot,
timestampFormat,
mode = "sidebar",
onClose,
}: PlanSidebarProps) {
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false);
const { copyToClipboard, isCopied } = useCopyToClipboard();
const planMarkdown = activeProposedPlan?.planMarkdown ?? null;
const displayedPlanMarkdown = planMarkdown ? stripDisplayedPlanMarkdown(planMarkdown) : null;
const planTitle = planMarkdown ? proposedPlanTitle(planMarkdown) : null;
const handleCopyPlan = useCallback(() => {
if (!planMarkdown) return;
copyToClipboard(planMarkdown);
}, [planMarkdown, copyToClipboard]);
const handleDownload = useCallback(() => {
if (!planMarkdown) return;
const filename = buildProposedPlanMarkdownFilename(planMarkdown);
downloadPlanAsTextFile(filename, normalizePlanMarkdownForExport(planMarkdown));
}, [planMarkdown]);
const handleSaveToWorkspace = useCallback(() => {
const api = readEnvironmentApi(environmentId);
if (!api || !workspaceRoot || !planMarkdown) return;
const filename = buildProposedPlanMarkdownFilename(planMarkdown);
setIsSavingToWorkspace(true);
void api.projects
.writeFile({
cwd: workspaceRoot,
relativePath: filename,
contents: normalizePlanMarkdownForExport(planMarkdown),
})
.then((result) => {
toastManager.add({
type: "success",
title: "Plan saved",
description: result.relativePath,
});
})
.catch((error) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Could not save plan",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
})
.then(
() => setIsSavingToWorkspace(false),
() => setIsSavingToWorkspace(false),
);
}, [environmentId, planMarkdown, workspaceRoot]);
return (
{/* Header */}
{label}
{activePlan ? (
{formatTimestamp(activePlan.createdAt, timestampFormat)}
) : null}
{planMarkdown ? (
) : null}
{/* Content */}
{/* Explanation */}
{activePlan?.explanation ? (
{activePlan.explanation}
) : null}
{/* Plan Steps */}
{activePlan && activePlan.steps.length > 0 ? (
Steps
{activePlan.steps.map((step) => (
{stepStatusIcon(step.status)}
{step.step}
))}
) : null}
{/* Proposed Plan Markdown */}
{planMarkdown ? (
{proposedPlanExpanded ? (
) : null}
) : null}
{/* Empty state */}
{!activePlan && !planMarkdown ? (
No active plan yet.
Plans will appear here when generated.
) : null}
);
});
export default PlanSidebar;
export type { PlanSidebarProps };