import { memo, useState, useCallback } from "react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import type { EnvironmentId, ScopedThreadRef } 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, } 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 { projectEnvironment } from "~/state/projects"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useAtomCommand } from "~/state/use-atom-command"; 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; threadRef?: ScopedThreadRef | undefined; markdownCwd: string | undefined; workspaceRoot: string | undefined; timestampFormat: TimestampFormat; mode?: "sheet" | "sidebar" | "embedded"; } const PlanSidebar = memo(function PlanSidebar({ activePlan, activeProposedPlan, label = "Plan", environmentId, threadRef, markdownCwd, workspaceRoot, timestampFormat, mode = "sidebar", }: PlanSidebarProps) { const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false); const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false); const writeProjectFile = useAtomCommand(projectEnvironment.writeFile, { reportFailure: false, }); const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "plan" }); 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(() => { if (!workspaceRoot || !planMarkdown) return; const filename = buildProposedPlanMarkdownFilename(planMarkdown); setIsSavingToWorkspace(true); void (async () => { const result = await writeProjectFile({ environmentId, input: { cwd: workspaceRoot, relativePath: filename, contents: normalizePlanMarkdownForExport(planMarkdown), }, }); setIsSavingToWorkspace(false); if (result._tag === "Success") { toastManager.add({ type: "success", title: "Plan saved", description: result.value.relativePath, }); return; } if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title: "Could not save plan", description: error instanceof Error ? error.message : "An error occurred.", }), ); } })(); }, [environmentId, planMarkdown, workspaceRoot, writeProjectFile]); 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 };