import { useEffect, useMemo, useRef, useState } from "react"; import { Link } from "react-router-dom"; import type { IssueComment, Agent } from "@paperclip/shared"; import { Button } from "@/components/ui/button"; import { Identity } from "./Identity"; import { MarkdownBody } from "./MarkdownBody"; import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor"; import { formatDateTime } from "../lib/utils"; interface CommentWithRunMeta extends IssueComment { runId?: string | null; runAgentId?: string | null; } interface CommentThreadProps { comments: CommentWithRunMeta[]; onAdd: (body: string, reopen?: boolean) => Promise; issueStatus?: string; agentMap?: Map; imageUploadHandler?: (file: File) => Promise; draftKey?: string; } const CLOSED_STATUSES = new Set(["done", "cancelled"]); const DRAFT_DEBOUNCE_MS = 800; function loadDraft(draftKey: string): string { try { return localStorage.getItem(draftKey) ?? ""; } catch { return ""; } } function saveDraft(draftKey: string, value: string) { try { if (value.trim()) { localStorage.setItem(draftKey, value); } else { localStorage.removeItem(draftKey); } } catch { // Ignore localStorage failures. } } function clearDraft(draftKey: string) { try { localStorage.removeItem(draftKey); } catch { // Ignore localStorage failures. } } export function CommentThread({ comments, onAdd, issueStatus, agentMap, imageUploadHandler, draftKey }: CommentThreadProps) { const [body, setBody] = useState(""); const [reopen, setReopen] = useState(true); const [submitting, setSubmitting] = useState(false); const editorRef = useRef(null); const draftTimer = useRef | null>(null); const isClosed = issueStatus ? CLOSED_STATUSES.has(issueStatus) : false; // Display oldest-first const sorted = useMemo( () => [...comments].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()), [comments], ); // Build mention options from agent map (exclude terminated agents) const mentions = useMemo(() => { if (!agentMap) return []; return Array.from(agentMap.values()) .filter((a) => a.status !== "terminated") .map((a) => ({ id: a.id, name: a.name, })); }, [agentMap]); useEffect(() => { if (!draftKey) return; setBody(loadDraft(draftKey)); }, [draftKey]); useEffect(() => { if (!draftKey) return; if (draftTimer.current) clearTimeout(draftTimer.current); draftTimer.current = setTimeout(() => { saveDraft(draftKey, body); }, DRAFT_DEBOUNCE_MS); }, [body, draftKey]); useEffect(() => { return () => { if (draftTimer.current) clearTimeout(draftTimer.current); }; }, []); async function handleSubmit() { const trimmed = body.trim(); if (!trimmed) return; setSubmitting(true); try { await onAdd(trimmed, isClosed && reopen ? true : undefined); setBody(""); if (draftKey) clearDraft(draftKey); setReopen(false); } finally { setSubmitting(false); } } return (

Comments ({comments.length})

{comments.length === 0 && (

No comments yet.

)}
{sorted.map((comment) => (
{comment.authorAgentId ? ( ) : ( )} {formatDateTime(comment.createdAt)}
{comment.body} {comment.runId && comment.runAgentId && (
run {comment.runId.slice(0, 8)}
)}
))}
{isClosed && ( )}
); }