import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ExecutionWorkspace } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { Loader2 } from "lucide-react"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { useToast } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; import { formatDateTime, issueUrl } from "../lib/utils"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./ui/dialog"; type ExecutionWorkspaceCloseDialogProps = { workspaceId: string; workspaceName: string; currentStatus: ExecutionWorkspace["status"]; open: boolean; onOpenChange: (open: boolean) => void; onClosed?: (workspace: ExecutionWorkspace) => void; }; function readinessTone(state: "ready" | "ready_with_warnings" | "blocked") { if (state === "blocked") { return "border-destructive/30 bg-destructive/5 text-destructive"; } if (state === "ready_with_warnings") { return "border-amber-500/30 bg-amber-500/10 text-amber-800 dark:text-amber-300"; } return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; } export function ExecutionWorkspaceCloseDialog({ workspaceId, workspaceName, currentStatus, open, onOpenChange, onClosed, }: ExecutionWorkspaceCloseDialogProps) { const queryClient = useQueryClient(); const { pushToast } = useToast(); const actionLabel = currentStatus === "cleanup_failed" ? "Retry close" : "Close workspace"; const readinessQuery = useQuery({ queryKey: queryKeys.executionWorkspaces.closeReadiness(workspaceId), queryFn: () => executionWorkspacesApi.getCloseReadiness(workspaceId), enabled: open, }); const closeWorkspace = useMutation({ mutationFn: () => executionWorkspacesApi.update(workspaceId, { status: "archived" }), onSuccess: (workspace) => { queryClient.setQueryData(queryKeys.executionWorkspaces.detail(workspace.id), workspace); queryClient.invalidateQueries({ queryKey: queryKeys.executionWorkspaces.closeReadiness(workspace.id) }); pushToast({ title: currentStatus === "cleanup_failed" ? "Workspace close retried" : "Workspace closed", tone: "success", }); onOpenChange(false); onClosed?.(workspace); }, onError: (error) => { pushToast({ title: "Failed to close workspace", body: error instanceof Error ? error.message : "Unknown error", tone: "error", }); }, }); const readiness = readinessQuery.data ?? null; const blockingIssues = readiness?.linkedIssues.filter((issue) => !issue.isTerminal) ?? []; const otherLinkedIssues = readiness?.linkedIssues.filter((issue) => issue.isTerminal) ?? []; const confirmDisabled = currentStatus === "archived" || closeWorkspace.isPending || readinessQuery.isLoading || readiness == null || readiness.state === "blocked"; return ( { if (!closeWorkspace.isPending) onOpenChange(nextOpen); }}> {actionLabel} Archive {workspaceName} and clean up any owned workspace artifacts. Paperclip keeps the workspace record and issue history, but removes it from active workspace views. {readinessQuery.isLoading ? (
Checking whether this workspace is safe to close...
) : readinessQuery.error ? (
{readinessQuery.error instanceof Error ? readinessQuery.error.message : "Failed to inspect workspace close readiness."}
) : readiness ? (
{readiness.state === "blocked" ? "Close is blocked" : readiness.state === "ready_with_warnings" ? "Close is allowed with warnings" : "Close is ready"}
{readiness.isSharedWorkspace ? "This is a shared workspace session. Archiving it removes this session record but keeps the underlying project workspace." : readiness.git?.workspacePath && readiness.git.repoRoot && readiness.git.workspacePath !== readiness.git.repoRoot ? "This execution workspace has its own checkout path and can be archived independently." : readiness.isProjectPrimaryWorkspace ? "This execution workspace currently points at the project's primary workspace path." : "This workspace is disposable and can be archived."}
{blockingIssues.length > 0 ? (

Blocking issues

{blockingIssues.map((issue) => (
{issue.identifier ?? issue.id} · {issue.title} {issue.status}
))}
) : null} {readiness.blockingReasons.length > 0 ? (

Blocking reasons

    {readiness.blockingReasons.map((reason, idx) => (
  • {reason}
  • ))}
) : null} {readiness.warnings.length > 0 ? (

Warnings

    {readiness.warnings.map((warning, idx) => (
  • {warning}
  • ))}
) : null} {readiness.git ? (

Git status

Branch
{readiness.git.branchName ?? "Unknown"}
Base ref
{readiness.git.baseRef ?? "Not set"}
Merged into base
{readiness.git.isMergedIntoBase == null ? "Unknown" : readiness.git.isMergedIntoBase ? "Yes" : "No"}
Ahead / behind
{(readiness.git.aheadCount ?? 0).toString()} / {(readiness.git.behindCount ?? 0).toString()}
Dirty tracked files
{readiness.git.dirtyEntryCount}
Untracked files
{readiness.git.untrackedEntryCount}
) : null} {otherLinkedIssues.length > 0 ? (

Other linked issues

{otherLinkedIssues.map((issue) => (
{issue.identifier ?? issue.id} · {issue.title} {issue.status}
))}
) : null} {readiness.runtimeServices.length > 0 ? (

Attached runtime services

{readiness.runtimeServices.map((service) => (
{service.serviceName} {service.status} · {service.lifecycle}
{service.url ?? service.command ?? service.cwd ?? "No additional details"}
))}
) : null}

Cleanup actions

{readiness.plannedActions.map((action, index) => (
{action.label}
{action.description}
{action.command ? (
                        {action.command}
                      
) : null}
))}
{currentStatus === "cleanup_failed" ? (
Cleanup previously failed on this workspace. Retrying close will rerun the cleanup flow and update the workspace status if it succeeds.
) : null} {currentStatus === "archived" ? (
This workspace is already archived.
) : null} {readiness.git?.repoRoot ? (
Repo root: {readiness.git.repoRoot} {readiness.git.workspacePath ? ( <> {" · "}Workspace path: {readiness.git.workspacePath} ) : null}
) : null}
Last checked {formatDateTime(new Date())}
) : null}
); }