import { memo, useState, useId } from "react"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts"; import { buildCollapsedProposedPlanPreviewMarkdown, buildProposedPlanMarkdownFilename, downloadPlanAsTextFile, normalizePlanMarkdownForExport, proposedPlanTitle, stripDisplayedPlanMarkdown, } from "../../proposedPlan"; import ChatMarkdown from "../ChatMarkdown"; import { EllipsisIcon } from "lucide-react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { cn } from "~/lib/utils"; import { Badge } from "../ui/badge"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "../ui/dialog"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { projectEnvironment } from "~/state/projects"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useAtomCommand } from "~/state/use-atom-command"; export const ProposedPlanCard = memo(function ProposedPlanCard({ planMarkdown, environmentId, threadRef, cwd, workspaceRoot, }: { planMarkdown: string; environmentId: EnvironmentId; threadRef?: ScopedThreadRef | undefined; cwd: string | undefined; workspaceRoot: string | undefined; }) { const [expanded, setExpanded] = useState(false); const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); const [savePath, setSavePath] = useState(""); const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false); const writeProjectFile = useAtomCommand(projectEnvironment.writeFile, { reportFailure: false, }); const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "plan", onError: (error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not copy plan", description: error instanceof Error ? error.message : "An error occurred while copying.", }), ); }, }); const savePathInputId = useId(); const title = proposedPlanTitle(planMarkdown) ?? "Proposed plan"; const lineCount = planMarkdown.split("\n").length; const canCollapse = planMarkdown.length > 900 || lineCount > 20; const displayedPlanMarkdown = stripDisplayedPlanMarkdown(planMarkdown); const collapsedPreview = canCollapse ? buildCollapsedProposedPlanPreviewMarkdown(planMarkdown, { maxLines: 10 }) : null; const downloadFilename = buildProposedPlanMarkdownFilename(planMarkdown); const saveContents = normalizePlanMarkdownForExport(planMarkdown); const handleDownload = () => { downloadPlanAsTextFile(downloadFilename, saveContents); }; const handleCopyPlan = () => { copyToClipboard(saveContents); }; const openSaveDialog = () => { if (!workspaceRoot) { toastManager.add( stackedThreadToast({ type: "error", title: "Workspace path is unavailable", description: "This thread does not have a workspace path to save into.", }), ); return; } setSavePath((existing) => (existing.length > 0 ? existing : downloadFilename)); setIsSaveDialogOpen(true); }; const handleSaveToWorkspace = () => { const relativePath = savePath.trim(); if (!workspaceRoot) { return; } if (!relativePath) { toastManager.add({ type: "warning", title: "Enter a workspace path", }); return; } setIsSavingToWorkspace(true); void (async () => { const result = await writeProjectFile({ environmentId, input: { cwd: workspaceRoot, relativePath, contents: saveContents, }, }); setIsSavingToWorkspace(false); if (result._tag === "Success") { setIsSaveDialogOpen(false); toastManager.add({ type: "success", title: "Plan saved to workspace", 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 while saving.", }), ); } })(); }; return (
{title}