import { memo, useState, useId } from "react"; 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 { readEnvironmentApi } from "~/environmentApi"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; 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 { copyToClipboard, isCopied } = useCopyToClipboard({ 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 api = readEnvironmentApi(environmentId); const relativePath = savePath.trim(); if (!api || !workspaceRoot) { return; } if (!relativePath) { toastManager.add({ type: "warning", title: "Enter a workspace path", }); return; } setIsSavingToWorkspace(true); void api.projects .writeFile({ cwd: workspaceRoot, relativePath, contents: saveContents, }) .then((result) => { setIsSaveDialogOpen(false); toastManager.add({ type: "success", title: "Plan saved to workspace", description: result.relativePath, }); }) .catch((error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not save plan", description: error instanceof Error ? error.message : "An error occurred while saving.", }), ); }) .then( () => { setIsSavingToWorkspace(false); }, () => { setIsSavingToWorkspace(false); }, ); }; return (
Plan

{title}

} > {isCopied ? "Copied!" : "Copy to clipboard"} Download as markdown Save to workspace
{canCollapse && !expanded ? ( ) : ( )} {canCollapse && !expanded ? (
) : null}
{canCollapse ? (
) : null}
{ if (!isSavingToWorkspace) { setIsSaveDialogOpen(open); } }} > Save plan to workspace Enter a path relative to {workspaceRoot ?? "the workspace"}.
); });