"use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, Pen } from "lucide-react"; import { useCallback, useEffect, 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"; import type { HitlDecision, InterruptResult } from "@/lib/hitl"; import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; interface LinearLabel { id: string; name: string; color: string; } interface LinearState { id: string; name: string; type: string; color: string; } interface LinearMember { id: string; name: string; displayName: string; email: string; active: boolean; } interface LinearPriority { priority: number; label: string; } type LinearUpdateIssueContext = { workspace?: { id: number; organization_name: string }; priorities?: LinearPriority[]; issue?: { id: string; identifier: string; title: string; description?: string; priority: number; url: string; current_state?: LinearState; current_assignee?: { id: string; name: string; email: string } | null; current_labels?: LinearLabel[]; team_id: string; document_id: number; }; team?: { id: string; name: string; key: string; states: LinearState[]; members: LinearMember[]; labels: LinearLabel[]; }; error?: string; }; interface SuccessResult { status: "success"; identifier: string; url: string; message?: string; } interface ErrorResult { status: "error"; message: string; } interface NotFoundResult { status: "not_found"; message: string; } interface AuthErrorResult { status: "auth_error"; message: string; connector_id?: number; connector_type: string; } type UpdateLinearIssueResult = | InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult; function isErrorResult(result: unknown): result is ErrorResult { return ( typeof result === "object" && result !== null && "status" in result && (result as ErrorResult).status === "error" ); } function isNotFoundResult(result: unknown): result is NotFoundResult { return ( typeof result === "object" && result !== null && "status" in result && (result as NotFoundResult).status === "not_found" ); } 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: { issue_ref: string; new_title?: string; new_description?: string; new_state_name?: string; new_assignee_email?: string; new_priority?: number; new_label_names?: string[]; }; interruptData: InterruptResult; onDecision: (decision: HitlDecision) => void; }) { const { phase, setProcessing, setRejected } = useHitlPhase(interruptData); const actionArgs = interruptData.action_requests[0]?.args ?? {}; const context = interruptData.context; const team = context?.team; const priorities = context?.priorities ?? []; const issue = context?.issue; const [isPanelOpen, setIsPanelOpen] = useState(false); const [editedArgs, setEditedArgs] = useState(() => ({ title: actionArgs.new_title ? String(actionArgs.new_title) : (issue?.title ?? args.new_title ?? ""), description: actionArgs.new_description ? String(actionArgs.new_description) : (issue?.description ?? args.new_description ?? ""), stateId: actionArgs.new_state_id ? String(actionArgs.new_state_id) : (issue?.current_state?.id ?? "__none__"), assigneeId: actionArgs.new_assignee_id ? String(actionArgs.new_assignee_id) : (issue?.current_assignee?.id ?? "__none__"), priority: actionArgs.new_priority != null ? String(actionArgs.new_priority) : String(issue?.priority ?? 0), labelIds: Array.isArray(actionArgs.new_label_ids) ? (actionArgs.new_label_ids as string[]) : (issue?.current_labels?.map((l) => l.id) ?? []), })); const [hasPanelEdits, setHasPanelEdits] = useState(false); const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom); const reviewConfig = interruptData.review_configs[0]; const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"]; const canEdit = allowedDecisions.includes("edit"); function resolveStateName(stateId: string | null) { if (!stateId || stateId === "__none__") return null; return team?.states.find((s) => s.id === stateId)?.name ?? stateId; } function resolveAssigneeName(assigneeId: string | null) { if (!assigneeId || assigneeId === "__none__") return null; const m = team?.members.find((m) => m.id === assigneeId); return m ? `${m.name} (${m.email})` : assigneeId; } function resolvePriorityLabel(p: string | null) { if (!p || p === "__none__") return null; return priorities.find((pr) => String(pr.priority) === p)?.label ?? p; } function resolveLabelNames(ids: string[]) { return ids.map((id) => team?.labels.find((l) => l.id === id)).filter(Boolean) as LinearLabel[]; } const buildFinalArgs = useCallback(() => { const labelsWereProposed = Array.isArray(actionArgs.new_label_ids); return { issue_id: issue?.id, document_id: issue?.document_id, connector_id: context?.workspace?.id, new_title: editedArgs.title || null, new_description: editedArgs.description || null, new_state_id: editedArgs.stateId === "__none__" ? null : editedArgs.stateId, new_assignee_id: editedArgs.assigneeId === "__none__" ? null : editedArgs.assigneeId, new_priority: Number(editedArgs.priority), new_label_ids: labelsWereProposed || editedArgs.labelIds.length > 0 ? editedArgs.labelIds : null, }; }, [actionArgs.new_label_ids, issue?.id, issue?.document_id, context?.workspace?.id, editedArgs]); const proposedStateName = resolveStateName( actionArgs.new_state_id ? String(actionArgs.new_state_id) : null ); const proposedAssigneeName = resolveAssigneeName( actionArgs.new_assignee_id ? String(actionArgs.new_assignee_id) : null ); const proposedPriorityLabel = resolvePriorityLabel( actionArgs.new_priority != null ? String(actionArgs.new_priority) : null ); const proposedLabelObjects = resolveLabelNames( Array.isArray(actionArgs.new_label_ids) ? (actionArgs.new_label_ids as string[]) : [] ); const hasProposedChanges = actionArgs.new_title || args.new_title || actionArgs.new_description || args.new_description || proposedStateName || proposedAssigneeName || proposedPriorityLabel || proposedLabelObjects.length > 0; const handleApprove = useCallback(() => { if (phase !== "pending") return; if (isPanelOpen) return; if (!allowedDecisions.includes("approve")) return; const isEdited = hasPanelEdits; setProcessing(); onDecision({ type: isEdited ? "edit" : "approve", edited_action: { name: interruptData.action_requests[0].name, args: buildFinalArgs(), }, }); }, [ phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits, ]); 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 Update Rejected" : phase === "processing" || phase === "complete" ? "Linear Issue Update Approved" : "Update Linear Issue"}

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

{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}

) : phase === "rejected" ? (

Issue update was cancelled

) : (

Requires your approval to proceed

)}
{phase === "pending" && canEdit && ( )}
{/* Context section — workspace + current issue + pickers in pending */} {phase === "pending" && ( <>
{context?.error ? (

{context.error}

) : ( <> {context?.workspace && (

Linear Account

{context.workspace.organization_name}
)} {issue && (

Current Issue

{issue.identifier}: {issue.title}
{issue.current_state && ( {issue.current_state.name} )} {issue.current_assignee && {issue.current_assignee.name}} {priorities.find((p) => p.priority === issue.priority) && ( {priorities.find((p) => p.priority === issue.priority)?.label} )}
{issue.current_labels && issue.current_labels.length > 0 && (
{issue.current_labels.map((label) => ( {label.name} ))}
)} {issue.url && ( Open in Linear ↗ )}
)} {team && ( <>

State

Assignee

Priority

{team.labels.length > 0 && (

Labels

setEditedArgs({ ...editedArgs, labelIds: value }) } className="flex flex-wrap gap-1.5" > {team.labels.map((label) => { const isSelected = editedArgs.labelIds.includes(label.id); return ( {label.name} ); })}
)} )} )}
)} {/* Content preview — proposed changes */}
{hasProposedChanges || hasPanelEdits ? ( <> {(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (

{String( hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title) )}

)} {(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
)} {proposedStateName && (
State → {proposedStateName}
)} {proposedAssigneeName && (
Assignee → {proposedAssigneeName}
)} {proposedPriorityLabel && (
Priority → {proposedPriorityLabel}
)} {proposedLabelObjects.length > 0 && (
{proposedLabelObjects.map((label) => ( {label.name} ))}
)} ) : (

No changes proposed

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

Linear authentication expired

{result.message}

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

Failed to update Linear issue

{result.message}

); } function NotFoundCard({ result }: { result: NotFoundResult }) { return (

Issue not found

{result.message}

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

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

Identifier: {result.identifier}
{result.url && ( )}
); } export const UpdateLinearIssueToolUI = ({ args, result, }: ToolCallMessagePartProps< { issue_ref: string; new_title?: string; new_description?: string; new_state_name?: string; new_assignee_email?: string; new_priority?: number; new_label_names?: string[]; }, UpdateLinearIssueResult >) => { const { dispatch } = useHitlDecision(); if (!result) return null; if (isInterruptResult(result)) { return ( } onDecision={(decision) => dispatch([decision])} /> ); } if ( typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected" ) { return null; } if (isNotFoundResult(result)) return ; if (isAuthErrorResult(result)) return ; if (isErrorResult(result)) return ; return ; };