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 ? ( } > {isCopied ? "Copied!" : "Copy to clipboard"} Download as markdown Save to workspace ) : 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 };