import { useCallback, useEffect, 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, projectWorkspaceUrl } 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: { executionWorkspaceId: string | null; executionWorkspacePreference: string | null; executionWorkspaceSettings: Issue["executionWorkspaceSettings"]; currentExecutionWorkspace?: ExecutionWorkspace | null; }) { 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 configuredWorkspaceLabel( selection: string | null | undefined, reusableWorkspace: ExecutionWorkspace | null, ) { switch (selection) { case "isolated_workspace": return "New isolated workspace"; case "reuse_existing": return reusableWorkspace?.mode === "isolated_workspace" ? "Existing isolated workspace" : "Reuse existing workspace"; default: return "Project default"; } } function projectWorkspaceDetailLink(input: { projectId: string | null | undefined; projectWorkspaceId: string | null | undefined; }) { if (!input.projectId || !input.projectWorkspaceId) return null; return projectWorkspaceUrl({ id: input.projectId, urlKey: input.projectId }, input.projectWorkspaceId); } function workspaceDetailLink(input: { projectId: string | null | undefined; issueProjectWorkspaceId: string | null | undefined; workspace: ExecutionWorkspace | null | undefined; }) { const linkedProjectWorkspaceId = input.workspace?.projectWorkspaceId ?? input.issueProjectWorkspaceId ?? null; if (input.workspace?.mode === "shared_workspace") { return projectWorkspaceDetailLink({ projectId: input.projectId, projectWorkspaceId: linkedProjectWorkspaceId, }); } return input.workspace ? `/execution-workspaces/${input.workspace.id}` : null; } 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: Omit< Pick< Issue, | "companyId" | "projectId" | "projectWorkspaceId" | "executionWorkspaceId" | "executionWorkspacePreference" | "executionWorkspaceSettings" >, "companyId" > & { companyId: string | null; currentExecutionWorkspace?: ExecutionWorkspace | null; }; project: { id: string; executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null; defaultProjectWorkspaceId?: string | null } | null; workspaces?: Array<{ id: string; isPrimary: boolean }> } | null; onUpdate: (data: Record) => void; initialEditing?: boolean; livePreview?: boolean; onDraftChange?: (data: Record, meta: { canSave: boolean }) => void; } export function IssueWorkspaceCard({ issue, project, onUpdate, initialEditing = false, livePreview = false, onDraftChange, }: IssueWorkspaceCardProps) { const { selectedCompanyId } = useCompany(); const companyId = issue.companyId ?? selectedCompanyId; const [editing, setEditing] = useState(initialEditing); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); const policyEnabled = experimentalSettings?.enableIsolatedWorkspaces === true && Boolean(project?.executionWorkspacePolicy?.enabled); const workspace = issue.currentExecutionWorkspace as ExecutionWorkspace | null | undefined; 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 configuredSelection = shouldPresentExistingWorkspaceSelection(issue) ? "reuse_existing" : ( issue.executionWorkspacePreference ?? issue.executionWorkspaceSettings?.mode ?? defaultExecutionWorkspaceModeForProject(project) ); const currentSelection = configuredSelection === "operator_branch" || configuredSelection === "agent_default" ? "shared_workspace" : configuredSelection; const [draftSelection, setDraftSelection] = useState(currentSelection); const [draftExecutionWorkspaceId, setDraftExecutionWorkspaceId] = useState(issue.executionWorkspaceId ?? ""); useEffect(() => { if (editing) return; setDraftSelection(currentSelection); setDraftExecutionWorkspaceId(issue.executionWorkspaceId ?? ""); }, [currentSelection, editing, issue.executionWorkspaceId]); const activeNonDefaultWorkspace = Boolean(workspace && workspace.mode !== "shared_workspace"); const configuredReusableWorkspace = deduplicatedReusableWorkspaces.find((w) => w.id === draftExecutionWorkspaceId) ?? (draftExecutionWorkspaceId === issue.executionWorkspaceId ? selectedReusableExecutionWorkspace : null); const selectedReusableWorkspaceLink = workspaceDetailLink({ projectId: project?.id, issueProjectWorkspaceId: issue.projectWorkspaceId, workspace: selectedReusableExecutionWorkspace, }); const currentWorkspaceLink = workspaceDetailLink({ projectId: project?.id, issueProjectWorkspaceId: issue.projectWorkspaceId, workspace, }); const canSaveWorkspaceConfig = draftSelection !== "reuse_existing" || draftExecutionWorkspaceId.length > 0; const buildWorkspaceDraftUpdate = useCallback(() => ({ executionWorkspacePreference: draftSelection, executionWorkspaceId: draftSelection === "reuse_existing" ? draftExecutionWorkspaceId || null : null, executionWorkspaceSettings: { mode: draftSelection === "reuse_existing" ? issueModeForExistingWorkspace(configuredReusableWorkspace?.mode) : draftSelection, }, }), [ configuredReusableWorkspace?.mode, draftExecutionWorkspaceId, draftSelection, ]); useEffect(() => { if (!onDraftChange) return; onDraftChange(buildWorkspaceDraftUpdate(), { canSave: canSaveWorkspaceConfig }); }, [buildWorkspaceDraftUpdate, canSaveWorkspaceConfig, onDraftChange]); const handleSave = useCallback(() => { if (!canSaveWorkspaceConfig) return; onUpdate(buildWorkspaceDraftUpdate()); setEditing(false); }, [ buildWorkspaceDraftUpdate, canSaveWorkspaceConfig, onUpdate, ]); const handleCancel = useCallback(() => { setDraftSelection(currentSelection); setDraftExecutionWorkspaceId(issue.executionWorkspaceId ?? ""); setEditing(false); }, [currentSelection, issue.executionWorkspaceId]); if (!policyEnabled || !project) return null; const showEditingControls = livePreview || editing; return (
{/* Header row */}
{activeNonDefaultWorkspace && workspace ? workspaceModeLabel(workspace.mode) : configuredWorkspaceLabel(currentSelection, selectedReusableExecutionWorkspace)} {workspace ? statusBadge(workspace.status) : statusBadge("idle")}
{!livePreview && editing ? ( <> ) : !livePreview ? ( ) : null}
{/* Read-only info */} {!showEditingControls && (
{workspace?.branchName && (
)} {workspace?.cwd && (
)} {workspace?.repoUrl && (
Repo:
)} {!workspace && (
{currentSelection === "isolated_workspace" ? "A fresh isolated workspace will be created when this issue runs." : currentSelection === "reuse_existing" ? "This issue will reuse an existing workspace when it runs." : "This issue will use the project default workspace configuration when it runs."}
)} {currentSelection === "reuse_existing" && selectedReusableExecutionWorkspace && (
Reusing:{" "} {selectedReusableWorkspaceLink ? ( ) : ( )}
)} {workspace && currentWorkspaceLink && (
View workspace details →
)}
)} {/* Editing controls */} {showEditingControls && (
{draftSelection === "reuse_existing" && ( )} {/* Current workspace summary when editing */} {workspace && (
Current:{" "} {currentWorkspaceLink ? ( ) : ( )} {" · "} {workspace.status}
)}
)}
); }