"use client"; import Link from "next/link"; import { useEffect, useState } from "react"; import { ExternalLink } from "lucide-react"; import { useListAgentsApiV1AgentsGet, type listAgentsApiV1AgentsGetResponse, } from "@/api/generated/agents/agents"; import { useListBoardsApiV1BoardsGet, type listBoardsApiV1BoardsGetResponse, } from "@/api/generated/boards/boards"; import { ApiError } from "@/api/mutator"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import type { ForgejoIssue, AssignIssueAgentResponse } from "@/lib/api-forgejo"; import { assignIssueToAgent, getLinkedBoardsForRepository, linkBoardForgejoRepository, } from "@/lib/api-forgejo"; type LinkedBoard = { id: string; name: string }; type AssignIssueAgentDialogProps = { issue: ForgejoIssue | null; repositoryName: string; open: boolean; onOpenChange: (open: boolean) => void; onSuccess: (result: AssignIssueAgentResponse) => void; }; function buildDefaultAgentPrompt(issue: ForgejoIssue, repositoryName: string) { const issueBody = (issue.body ?? issue.body_preview ?? "").trim(); const lines = [ `Work on ${repositoryName}#${issue.forgejo_issue_number}: ${issue.title}`, "", "Use this Forgejo issue as the source of truth.", `Issue URL: ${issue.html_url}`, ]; if (issueBody) { lines.push("", "Issue details:", issueBody); } lines.push( "", "Expected approach:", "- Read the relevant Pipeline code before editing.", "- Keep the work scoped to this issue.", "- Run targeted validation and report what passed.", "- Post progress, blockers, and handoff notes back to the Pipeline task.", ); return lines.join("\n"); } export function AssignIssueAgentDialog({ issue, repositoryName, open, onOpenChange, onSuccess, }: AssignIssueAgentDialogProps) { const [boardId, setBoardId] = useState(""); const [agentId, setAgentId] = useState(""); const [priority, setPriority] = useState("medium"); const [instructions, setInstructions] = useState(""); const [startImmediately, setStartImmediately] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [linkedBoards, setLinkedBoards] = useState([]); const [boardsLoading, setBoardsLoading] = useState(false); const [linkedBoardsLoaded, setLinkedBoardsLoaded] = useState(false); const [linkBoardId, setLinkBoardId] = useState(""); const [isLinkingRepository, setIsLinkingRepository] = useState(false); // Fetch only boards linked to this issue's repository — prevents picking a // board that the backend will reject with "not linked to any board". useEffect(() => { if (!open || !issue) return; let cancelled = false; setBoardsLoading(true); setLinkedBoardsLoaded(false); setLinkedBoards([]); setBoardId(""); getLinkedBoardsForRepository(issue.repository_id) .then((boards) => { if (cancelled) return; setLinkedBoards(boards); if (boards.length === 1) setBoardId(boards[0].id); }) .catch(() => { if (!cancelled) setLinkedBoards([]); }) .finally(() => { if (!cancelled) { setBoardsLoading(false); setLinkedBoardsLoaded(true); } }); return () => { cancelled = true; }; }, [open, issue]); const agentsQuery = useListAgentsApiV1AgentsGet< listAgentsApiV1AgentsGetResponse, ApiError >( { board_id: boardId || undefined, limit: 200 }, { query: { enabled: open && !!boardId, refetchOnMount: "always" } }, ); const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError >( { limit: 200 }, { query: { enabled: open && linkedBoardsLoaded && !boardsLoading && linkedBoards.length === 0, refetchOnMount: "always", }, }, ); const agents = agentsQuery.data?.status === 200 ? (agentsQuery.data.data.items ?? []) : []; const allBoards = boardsQuery.data?.status === 200 ? (boardsQuery.data.data.items ?? []) : []; useEffect(() => { if (!open) { setBoardId(""); setAgentId(""); setPriority("medium"); setInstructions(""); setStartImmediately(true); setError(null); setLinkedBoardsLoaded(false); setLinkBoardId(""); setIsLinkingRepository(false); } }, [open]); useEffect(() => { if (!open || !issue) return; setInstructions(buildDefaultAgentPrompt(issue, repositoryName)); }, [open, issue, repositoryName]); useEffect(() => { setAgentId(""); }, [boardId]); if (!issue) return null; const noLinkedBoards = linkedBoardsLoaded && !boardsLoading && linkedBoards.length === 0; const prompt = instructions.trim(); const handleSubmit = async () => { if (!boardId || !agentId || !prompt) return; setIsSubmitting(true); setError(null); try { const result = await assignIssueToAgent(issue.id, { board_id: boardId, assigned_agent_id: agentId, priority, start_immediately: startImmediately, status: startImmediately ? "in_progress" : "inbox", instructions: prompt, }); onSuccess(result); onOpenChange(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to assign agent"); } finally { setIsSubmitting(false); } }; const handleLinkRepositoryToBoard = async () => { if (!issue || !linkBoardId) return; setIsLinkingRepository(true); setError(null); try { await linkBoardForgejoRepository(linkBoardId, issue.repository_id); const linkedBoard = allBoards.find((board) => board.id === linkBoardId); const nextBoard = { id: linkBoardId, name: linkedBoard?.name ?? "Selected board", }; setLinkedBoards([nextBoard]); setBoardId(nextBoard.id); setLinkBoardId(""); } catch (err) { setError( err instanceof Error ? err.message : "Failed to link repository to board", ); } finally { setIsLinkingRepository(false); } }; const toggleSwitch = ( value: boolean, setter: (v: boolean) => void, disabled: boolean, ) => ( ); return ( { if (!isSubmitting) onOpenChange(next); }} > Assign to agent Create a Pipeline task for{" "} {repositoryName}#{issue.forgejo_issue_number} {" "} and assign an agent to work on it.

{issue.title}

{noLinkedBoards ? (

Repository not linked to any board

Before you can assign an agent, link{" "} {repositoryName}{" "} to the board that should own this task. Open the target board and use its Git Project repositories panel, or manage tracked repositories first.

{boardsQuery.isError ? (

Unable to load boards. You can still manage repositories from Git Projects.

) : null}
Manage Git repositories
) : ( <>
{boardId && !agentsQuery.isLoading && agents.length === 0 ? (

No agents are available on this board yet.

) : null}