"use client"; import { makeAssistantToolUI } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, Pen } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useHitlPhase } from "@/hooks/use-hitl-phase"; interface LinearLabel { id: string; name: string; color: string; } interface LinearState { id: string; name: string; type: string; color: string; position: number; } interface LinearMember { id: string; name: string; displayName: string; email: string; active: boolean; } interface LinearTeam { id: string; name: string; key: string; states: LinearState[]; members: LinearMember[]; labels: LinearLabel[]; } interface LinearPriority { priority: number; label: string; } interface LinearWorkspace { id: number; name: string; organization_name: string; teams: LinearTeam[]; priorities: LinearPriority[]; auth_expired?: boolean; } interface InterruptResult { __interrupt__: true; __decided__?: "approve" | "reject" | "edit"; __completed__?: boolean; action_requests: Array<{ name: string; args: Record; }>; review_configs: Array<{ action_name: string; allowed_decisions: Array<"approve" | "edit" | "reject">; }>; interrupt_type?: string; context?: { workspaces?: LinearWorkspace[]; error?: string; }; } interface SuccessResult { status: "success"; issue_id: string; identifier: string; url: string; message?: string; } interface ErrorResult { status: "error"; message: string; } interface AuthErrorResult { status: "auth_error"; message: string; connector_id?: number; connector_type: string; } type CreateLinearIssueResult = InterruptResult | SuccessResult | ErrorResult | AuthErrorResult; function isInterruptResult(result: unknown): result is InterruptResult { return ( typeof result === "object" && result !== null && "__interrupt__" in result && (result as InterruptResult).__interrupt__ === true ); } function isErrorResult(result: unknown): result is ErrorResult { return ( typeof result === "object" && result !== null && "status" in result && (result as ErrorResult).status === "error" ); } function isAuthErrorResult(result: unknown): result is AuthErrorResult { return ( typeof result === "object" && result !== null && "status" in result && (result as AuthErrorResult).status === "auth_error" ); } function ApprovalCard({ args, interruptData, onDecision, }: { args: { title: string; description?: string }; interruptData: InterruptResult; onDecision: (decision: { type: "approve" | "reject" | "edit"; message?: string; edited_action?: { name: string; args: Record }; }) => void; }) { const { phase, setProcessing, setRejected } = useHitlPhase(interruptData); const [isPanelOpen, setIsPanelOpen] = useState(false); const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom); const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>( null ); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(""); const [selectedTeamId, setSelectedTeamId] = useState(""); const [selectedStateId, setSelectedStateId] = useState("__none__"); const [selectedAssigneeId, setSelectedAssigneeId] = useState("__none__"); const [selectedPriority, setSelectedPriority] = useState("0"); const [selectedLabelIds, setSelectedLabelIds] = useState([]); const workspaces = interruptData.context?.workspaces ?? []; const validWorkspaces = useMemo(() => workspaces.filter((w) => !w.auth_expired), [workspaces]); const expiredWorkspaces = useMemo(() => workspaces.filter((w) => w.auth_expired), [workspaces]); const selectedWorkspace = useMemo( () => validWorkspaces.find((w) => String(w.id) === selectedWorkspaceId) ?? null, [validWorkspaces, selectedWorkspaceId] ); const selectedTeam = useMemo( () => selectedWorkspace?.teams.find((t) => t.id === selectedTeamId) ?? null, [selectedWorkspace, selectedTeamId] ); const isTitleValid = (pendingEdits?.title ?? args.title ?? "").trim().length > 0; const canApprove = !!selectedWorkspaceId && !!selectedTeamId && isTitleValid; const reviewConfig = interruptData.review_configs[0]; const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; const canEdit = allowedDecisions.includes("edit"); const buildFinalArgs = useCallback( (overrides?: { title?: string; description?: string }) => { return { title: overrides?.title ?? pendingEdits?.title ?? args.title, description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null, connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null, team_id: selectedTeamId || null, state_id: selectedStateId === "__none__" ? null : selectedStateId, assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId, priority: Number(selectedPriority), label_ids: selectedLabelIds, }; }, [ args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits, ] ); const handleApprove = useCallback(() => { if (phase !== "pending") return; if (isPanelOpen || !canApprove) return; if (!allowedDecisions.includes("approve")) return; const isEdited = pendingEdits !== null; setProcessing(); onDecision({ type: isEdited ? "edit" : "approve", edited_action: { name: interruptData.action_requests[0].name, args: buildFinalArgs(), }, }); }, [ phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits, ]); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) { handleApprove(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [handleApprove]); return (
{/* Header */}

{phase === "rejected" ? "Linear Issue Rejected" : phase === "processing" || phase === "complete" ? "Linear Issue Approved" : "Create Linear Issue"}

{phase === "processing" ? ( ) : phase === "complete" ? (

{pendingEdits ? "Issue created with your changes" : "Issue created"}

) : phase === "rejected" ? (

Issue creation was cancelled

) : (

Requires your approval to proceed

)}
{phase === "pending" && canEdit && ( )}
{/* Context section — real pickers in pending */} {phase === "pending" && ( <>
{interruptData.context?.error ? (

{interruptData.context.error}

) : ( <> {workspaces.length > 0 && (

Linear Account *

)} {selectedWorkspace && ( <>

Team *

{selectedTeam && ( <>

State

Assignee

Priority

{selectedTeam.labels.length > 0 && (

Labels

{selectedTeam.labels.map((label) => { const isSelected = selectedLabelIds.includes(label.id); return ( {label.name} ); })}
)} )} )} )}
)} {/* Content preview */}
{(pendingEdits?.title ?? args.title) != null && (

{pendingEdits?.title ?? args.title}

)} {(pendingEdits?.description ?? args.description) != null && (
)}
{/* Action buttons - only shown when pending */} {phase === "pending" && ( <>
{allowedDecisions.includes("approve") && ( )} {allowedDecisions.includes("reject") && ( )}
)}
); } function AuthErrorCard({ result }: { result: AuthErrorResult }) { return (

All Linear accounts expired

{result.message}

); } function ErrorCard({ result }: { result: ErrorResult }) { return (

Failed to create Linear issue

{result.message}

); } function SuccessCard({ result }: { result: SuccessResult }) { return (

{result.message || "Linear issue created successfully"}

Identifier: {result.identifier}
{result.url && ( )}
); } export const CreateLinearIssueToolUI = makeAssistantToolUI< { title: string; description?: string }, CreateLinearIssueResult >({ toolName: "create_linear_issue", render: function CreateLinearIssueUI({ args, result }) { if (!result) return null; if (isInterruptResult(result)) { return ( { window.dispatchEvent( new CustomEvent("hitl-decision", { detail: { decisions: [decision] } }) ); }} /> ); } if ( typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected" ) { return null; } if (isAuthErrorResult(result)) return ; if (isErrorResult(result)) return ; return ; }, });