import { memo, useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type CSSProperties, type DragEvent, type RefObject } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import type { AgentEnvConfig, EnvBinding, 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 { MissingUserSecretsBanner } from "../pages/secrets/MissingUserSecretsBanner"; 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, isAgentTaskTarget } from "../lib/company-members"; import { queryKeys } from "../lib/queryKeys"; import { orderReusableExecutionWorkspaces } from "../lib/reusable-execution-workspaces"; import { defaultExecutionWorkspaceModeForProject, defaultProjectWorkspaceIdForProject, issueExecutionWorkspaceModeForExistingWorkspace, } from "../lib/project-workspace-defaults"; 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 { isIssueWorkMode, nextWorkMode, workModeMetaFor, workModeMetaList } from "../lib/work-mode-meta"; 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, Check, CircleDot, Minus, ArrowUp, ArrowDown, AlertTriangle, Tag, Calendar, Paperclip, FileText, Flag, Loader2, ListTree, X, Eye, ShieldAlert, ShieldCheck, ScanEye, } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; 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"; import { getTrustPreset } from "../lib/trust-policy-ui"; import { ReusableExecutionWorkspaceSelect } from "./ReusableExecutionWorkspaceSelect"; const DRAFT_KEY = "paperclip:issue-draft"; const DEBOUNCE_MS = 800; const MOBILE_DIALOG_HEIGHT = "calc(100dvh - max(1rem, env(safe-area-inset-top)) - max(1rem, env(safe-area-inset-bottom)))"; interface IssueDraft { title: string; description: string; status: string; priority: string; assigneeValue: string; reviewerValue: string; approverValue: string; watchdogAgentId?: string; watchdogInstructions?: 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 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`; } function buildStatusOptions(): ReadonlyArray<{ value: string; label: string; color: string; description?: string }> { const palette = issueStatusText; return [ { value: "backlog", label: "Backlog", color: palette.backlog ?? issueStatusTextDefault, description: "Parked - assignee will not be woken", }, { value: "todo", label: "Todo", color: palette.todo ?? issueStatusTextDefault, description: "Executable - assignee will be woken", }, { value: "in_progress", label: "In Progress", color: palette.in_progress ?? issueStatusTextDefault }, { value: "in_review", label: "In Review", color: palette.in_review ?? issueStatusTextDefault }, { value: "done", label: "Done", color: palette.done ?? issueStatusTextDefault }, ]; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function isRequiredUserSecretBinding(value: unknown): value is Extract { return isRecord(value) && value.type === "user_secret_ref" && typeof value.key === "string" && value.key.trim().length > 0 && value.required !== false && value.allowMissingOverride !== true; } function collectRequiredUserSecretKeysFromEnv(env: AgentEnvConfig | Record | null | undefined): string[] { if (!isRecord(env)) return []; return Object.values(env).flatMap((binding) => isRequiredUserSecretBinding(binding) ? [binding.key.trim()] : [], ); } function uniqueRequiredUserSecretKeys(inputs: Array | null | undefined>): string[] { return [...new Set(inputs.flatMap(collectRequiredUserSecretKeysFromEnv))]; } function shouldWarnAboutRunUserSecrets(status: string, assigneeAgentId: string | null | undefined) { return Boolean(assigneeAgentId) && (status === "todo" || status === "in_progress"); } 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 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 (