import { useCallback, useMemo, useRef, useState } from "react"; import { Link } from "@/lib/router"; import type { Issue, ExecutionWorkspace } from "@paperclipai/shared"; import { useQuery } from "@tanstack/react-query"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { instanceSettingsApi } from "../api/instanceSettings"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Check, Copy, GitBranch, FolderOpen, Pencil, X } from "lucide-react"; /* -------------------------------------------------------------------------- */ /* Utility helpers (mirrored from IssueProperties for self-containment) */ /* -------------------------------------------------------------------------- */ const EXECUTION_WORKSPACE_OPTIONS = [ { value: "shared_workspace", label: "Project default" }, { value: "isolated_workspace", label: "New isolated workspace" }, { value: "reuse_existing", label: "Reuse existing workspace" }, ] as const; function issueModeForExistingWorkspace(mode: string | null | undefined) { if (mode === "isolated_workspace" || mode === "operator_branch" || mode === "shared_workspace") return mode; if (mode === "adapter_managed" || mode === "cloud_sandbox") return "agent_default"; return "shared_workspace"; } function shouldPresentExistingWorkspaceSelection(issue: Issue) { const persistedMode = issue.currentExecutionWorkspace?.mode ?? issue.executionWorkspaceSettings?.mode ?? issue.executionWorkspacePreference; return Boolean( issue.executionWorkspaceId && (persistedMode === "isolated_workspace" || persistedMode === "operator_branch"), ); } function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined) { const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null; if (defaultMode === "isolated_workspace" || defaultMode === "operator_branch") return defaultMode; if (defaultMode === "adapter_default") return "agent_default"; return "shared_workspace"; } /* -------------------------------------------------------------------------- */ /* Sub-components */ /* -------------------------------------------------------------------------- */ function BreakablePath({ text }: { text: string }) { const parts: React.ReactNode[] = []; const segments = text.split(/(?<=[\/-])/); for (let i = 0; i < segments.length; i++) { if (i > 0) parts.push(); parts.push(segments[i]); } return <>{parts}; } function CopyableInline({ value, label, mono }: { value: string; label?: string; mono?: boolean }) { const [copied, setCopied] = useState(false); const timerRef = useRef>(undefined); const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(value); setCopied(true); clearTimeout(timerRef.current); timerRef.current = setTimeout(() => setCopied(false), 1500); } catch { /* noop */ } }, [value]); return ( {label && {label}} ); } function workspaceModeLabel(mode: string | null | undefined) { switch (mode) { case "isolated_workspace": return "Isolated workspace"; case "operator_branch": return "Operator branch"; case "cloud_sandbox": return "Cloud sandbox"; case "adapter_managed": return "Adapter managed"; default: return "Workspace"; } } function statusBadge(status: string) { const colors: Record = { active: "bg-green-500/15 text-green-700 dark:text-green-400", idle: "bg-muted text-muted-foreground", in_review: "bg-blue-500/15 text-blue-700 dark:text-blue-400", archived: "bg-muted text-muted-foreground", }; return ( {status.replace(/_/g, " ")} ); } /* -------------------------------------------------------------------------- */ /* Main component */ /* -------------------------------------------------------------------------- */ interface IssueWorkspaceCardProps { issue: Issue; project: { id: string; executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null; defaultProjectWorkspaceId?: string | null } | null; workspaces?: Array<{ id: string; isPrimary: boolean }> } | null; onUpdate: (data: Record) => void; } export function IssueWorkspaceCard({ issue, project, onUpdate }: IssueWorkspaceCardProps) { const { selectedCompanyId } = useCompany(); const companyId = issue.companyId ?? selectedCompanyId; const [editing, setEditing] = useState(false); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), }); const policyEnabled = experimentalSettings?.enableIsolatedWorkspaces === true && Boolean(project?.executionWorkspacePolicy?.enabled); const workspace = issue.currentExecutionWorkspace as ExecutionWorkspace | null | undefined; // Only show this card for non-default workspaces const isNonDefault = workspace && workspace.mode !== "shared_workspace"; const { data: reusableExecutionWorkspaces } = useQuery({ queryKey: queryKeys.executionWorkspaces.list(companyId!, { projectId: issue.projectId ?? undefined, projectWorkspaceId: issue.projectWorkspaceId ?? undefined, reuseEligible: true, }), queryFn: () => executionWorkspacesApi.list(companyId!, { projectId: issue.projectId ?? undefined, projectWorkspaceId: issue.projectWorkspaceId ?? undefined, reuseEligible: true, }), enabled: Boolean(companyId) && Boolean(issue.projectId) && editing, }); const deduplicatedReusableWorkspaces = useMemo(() => { const workspaces = reusableExecutionWorkspaces ?? []; const seen = new Map(); for (const ws of workspaces) { const key = ws.cwd ?? ws.id; const existing = seen.get(key); if (!existing || new Date(ws.lastUsedAt) > new Date(existing.lastUsedAt)) { seen.set(key, ws); } } return Array.from(seen.values()); }, [reusableExecutionWorkspaces]); const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find((w) => w.id === issue.executionWorkspaceId) ?? workspace ?? null; const currentSelection = shouldPresentExistingWorkspaceSelection(issue) ? "reuse_existing" : ( issue.executionWorkspacePreference ?? issue.executionWorkspaceSettings?.mode ?? defaultExecutionWorkspaceModeForProject(project) ); // Don't render if feature is off or workspace is default/absent if (!policyEnabled || !isNonDefault) return null; return (
{/* Header row */}
{workspaceModeLabel(workspace.mode)} {statusBadge(workspace.status)}
{/* Read-only info */} {!editing && (
{workspace.branchName && (
)} {workspace.cwd && (
)} {workspace.repoUrl && (
Repo:
)}
View workspace details →
)} {/* Editing controls */} {editing && (
{currentSelection === "reuse_existing" && ( )} {/* Current workspace summary when editing */} {workspace && (
Current:{" "} {" · "} {workspace.status}
)}
)}
); }