import { memo, useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent, type RefObject } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import type { IssueWorkMode } from "@paperclipai/shared"; import { pickTextColorForSolidBg } from "@/lib/color-contrast"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { issuesApi } from "../api/issues"; import { instanceSettingsApi } from "../api/instanceSettings"; import { projectsApi } from "../api/projects"; import { agentsApi } from "../api/agents"; import { accessApi } from "../api/access"; import { authApi } from "../api/auth"; import { assetsApi } from "../api/assets"; import { buildCompanyUserInlineOptions, buildMarkdownMentionOptions } from "../lib/company-members"; import { queryKeys } from "../lib/queryKeys"; import { orderReusableExecutionWorkspaces } from "../lib/reusable-execution-workspaces"; import { useProjectOrder } from "../hooks/useProjectOrder"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects"; import { buildExecutionPolicy } from "../lib/issue-execution-policy"; import { useToastActions } from "../context/ToastContext"; import { assigneeValueFromSelection, currentUserAssigneeOption, parseAssigneeValue, } from "../lib/assignees"; import { Dialog, DialogContent, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Maximize2, Minimize2, MoreHorizontal, ChevronRight, ChevronDown, CircleDot, ClipboardList, Hammer, Minus, ArrowUp, ArrowDown, AlertTriangle, Tag, Calendar, Paperclip, FileText, Flag, Loader2, ListTree, X, Eye, ShieldCheck, } from "lucide-react"; import { cn } from "../lib/utils"; import { extractProviderIdWithFallback } from "../lib/model-utils"; import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; import { AgentIcon } from "./AgentIconPicker"; import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector"; const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; interface IssueDraft { title: string; description: string; status: string; priority: string; assigneeValue: string; reviewerValue: string; approverValue: string; assigneeId?: string; projectId: string; projectWorkspaceId?: string; assigneeModelLane?: IssueModelLane; assigneeModelOverride: string; assigneeThinkingEffort: string; assigneeChrome: boolean; executionWorkspaceMode?: string; selectedExecutionWorkspaceId?: string; useIsolatedExecutionWorkspace?: boolean; workMode?: IssueWorkMode; } type StagedIssueFile = { id: string; file: File; kind: "document" | "attachment"; documentKey?: string; title?: string | null; }; import { buildAssigneeAdapterOverrides, ISSUE_OVERRIDE_ADAPTER_TYPES, type IssueModelLane, } from "../lib/issue-assignee-overrides"; const STAGED_FILE_ACCEPT = "image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown"; const ISSUE_THINKING_EFFORT_OPTIONS = { claude_local: [ { value: "", label: "Default" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ], codex_local: [ { value: "", label: "Default" }, { value: "minimal", label: "Minimal" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "xhigh", label: "X-High" }, ], opencode_local: [ { value: "", label: "Default" }, { value: "minimal", label: "Minimal" }, { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "xhigh", label: "X-High" }, { value: "max", label: "Max" }, ], } as const; function isIssueWorkMode(value: unknown): value is IssueWorkMode { return value === "standard" || value === "planning"; } const ISSUE_WORK_MODE_OPTIONS: ReadonlyArray<{ value: IssueWorkMode; label: string; icon: typeof Hammer; }> = [ { value: "standard", label: "Standard", icon: Hammer }, { value: "planning", label: "Planning", icon: ClipboardList }, ]; function loadDraft(): IssueDraft | null { try { const raw = localStorage.getItem(DRAFT_KEY); if (!raw) return null; return JSON.parse(raw) as IssueDraft; } catch { return null; } } function saveDraft(draft: IssueDraft) { localStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } function clearDraft() { localStorage.removeItem(DRAFT_KEY); } function isTextDocumentFile(file: File) { const name = file.name.toLowerCase(); return ( name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".txt") || file.type === "text/markdown" || file.type === "text/plain" ); } function fileBaseName(filename: string) { return filename.replace(/\.[^.]+$/, ""); } function slugifyDocumentKey(input: string) { const slug = input .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "document"; } function titleizeFilename(input: string) { return input .split(/[-_ ]+/g) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function createUniqueDocumentKey(baseKey: string, stagedFiles: StagedIssueFile[]) { const existingKeys = new Set( stagedFiles .filter((file) => file.kind === "document") .map((file) => file.documentKey) .filter((key): key is string => Boolean(key)), ); if (!existingKeys.has(baseKey)) return baseKey; let suffix = 2; while (existingKeys.has(`${baseKey}-${suffix}`)) { suffix += 1; } return `${baseKey}-${suffix}`; } function formatFileSize(file: File) { if (file.size < 1024) return `${file.size} B`; if (file.size < 1024 * 1024) return `${(file.size / 1024).toFixed(1)} KB`; return `${(file.size / (1024 * 1024)).toFixed(1)} MB`; } const statuses: ReadonlyArray<{ value: string; label: string; color: string; description?: string }> = [ { value: "backlog", label: "Backlog", color: issueStatusText.backlog ?? issueStatusTextDefault, description: "Parked — assignee will not be woken", }, { value: "todo", label: "Todo", color: issueStatusText.todo ?? issueStatusTextDefault, description: "Executable — assignee will be woken", }, { value: "in_progress", label: "In Progress", color: issueStatusText.in_progress ?? issueStatusTextDefault }, { value: "in_review", label: "In Review", color: issueStatusText.in_review ?? issueStatusTextDefault }, { value: "done", label: "Done", color: issueStatusText.done ?? issueStatusTextDefault }, ]; const priorities = [ { value: "critical", label: "Critical", icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault }, { value: "high", label: "High", icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault }, { value: "medium", label: "Medium", icon: Minus, color: priorityColor.medium ?? priorityColorDefault }, { value: "low", label: "Low", icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault }, ]; const EXECUTION_WORKSPACE_MODES = [ { value: "shared_workspace", label: "Project default" }, { value: "isolated_workspace", label: "New isolated workspace" }, { value: "reuse_existing", label: "Reuse existing workspace" }, ] as const; function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null } | null | undefined) { if (!project) return ""; return project.executionWorkspacePolicy?.defaultProjectWorkspaceId ?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id ?? project.workspaces?.[0]?.id ?? ""; } 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" || defaultMode === "adapter_default" ) { return defaultMode === "adapter_default" ? "agent_default" : defaultMode; } return "shared_workspace"; } function defaultExecutionWorkspaceModeForIssueDefaults( defaults: { executionWorkspaceId?: unknown; executionWorkspaceMode?: unknown; }, project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined, ) { if (typeof defaults.executionWorkspaceId === "string" && defaults.executionWorkspaceId.length > 0) { return "reuse_existing"; } return typeof defaults.executionWorkspaceMode === "string" && defaults.executionWorkspaceMode.length > 0 ? defaults.executionWorkspaceMode : defaultExecutionWorkspaceModeForProject(project); } const IssueTitleTextarea = memo(function IssueTitleTextarea({ value, pending, assigneeValue, projectId, descriptionEditorRef, assigneeSelectorRef, projectSelectorRef, onChange, }: { value: string; pending: boolean; assigneeValue: string; projectId: string; descriptionEditorRef: RefObject; assigneeSelectorRef: RefObject; projectSelectorRef: RefObject; onChange: (value: string) => void; }) { const [draftValue, setDraftValue] = useState(value); useEffect(() => { setDraftValue(value); }, [value]); return (