import { useCallback, useMemo, useState } from "react"; import type { Task, TaskComment } from "@fusion/core"; import { addComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../api"; import type { ToastType } from "../hooks/useToast"; type CommentType = "user" | "steering"; interface TaskCommentsProps { task: Task; onTaskUpdated?: (task: Task) => void; addToast: (message: string, type?: ToastType) => void; currentAuthor?: string; } function formatCommentTimestamp(comment: TaskComment): string { const timestamp = comment.updatedAt || comment.createdAt; const label = new Date(timestamp).toLocaleString(); return comment.updatedAt ? `${label} (edited)` : label; } function formatRelativeTimestamp(iso: string): string { const date = new Date(iso); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMin = Math.floor(diffMs / 60000); const diffHr = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHr / 24); if (diffMin < 1) return "just now"; if (diffMin < 60) return `${diffMin}m ago`; if (diffHr < 24) return `${diffHr}h ago`; if (diffDay < 7) return `${diffDay}d ago`; return date.toLocaleDateString(); } const MAX_LENGTH = 2000; export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user" }: TaskCommentsProps) { const [draft, setDraft] = useState(""); const [commentType, setCommentType] = useState("user"); const [editingId, setEditingId] = useState(null); const [editingText, setEditingText] = useState(""); const [submitting, setSubmitting] = useState(false); const [deletingId, setDeletingId] = useState(null); // Unified comments from task.comments (includes migrated steering comments) const comments = useMemo(() => task.comments || [], [task.comments]); // Legacy steering comments (if any still exist on the task) const steeringComments = useMemo(() => task.steeringComments || [], [task.steeringComments]); // All comments combined, sorted newest first const allComments = useMemo(() => { const combined = [...comments, ...steeringComments]; return combined.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); }, [comments, steeringComments]); // Determine if a comment is a steering/AI guidance comment const isSteeringComment = useCallback((comment: TaskComment): boolean => { // Check if from the steeringComments array if (steeringComments.some(sc => sc.id === comment.id)) return true; // Check if the author indicates it's an agent/AI comment if (comment.author === "agent" || comment.author === "system") return true; return false; }, [steeringComments]); const handleAddComment = useCallback(async () => { const text = draft.trim(); if (!text || text.length > MAX_LENGTH || submitting) return; setSubmitting(true); try { let updated: Task; if (commentType === "steering") { updated = await addComment(task.id, text); } else { updated = await addTaskComment(task.id, text, currentAuthor); } setDraft(""); onTaskUpdated?.(updated); addToast("Comment added", "success"); } catch (error: any) { addToast(error.message || "Failed to add comment", "error"); } finally { setSubmitting(false); } }, [draft, commentType, submitting, task.id, currentAuthor, onTaskUpdated, addToast]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); void handleAddComment(); } }, [handleAddComment] ); async function handleSaveEdit(commentId: string) { const text = editingText.trim(); if (!text) return; setSubmitting(true); try { const updated = await updateTaskComment(task.id, commentId, text); setEditingId(null); setEditingText(""); onTaskUpdated?.(updated); addToast("Comment updated", "success"); } catch (error: any) { addToast(error.message || "Failed to update comment", "error"); } finally { setSubmitting(false); } } async function handleDelete(commentId: string) { setDeletingId(commentId); try { const updated = await deleteTaskComment(task.id, commentId); onTaskUpdated?.(updated); addToast("Comment deleted", "success"); } catch (error: any) { addToast(error.message || "Failed to delete comment", "error"); } finally { setDeletingId(null); } } const isValid = draft.trim().length > 0 && draft.length <= MAX_LENGTH; return (

Comments

{allComments.length === 0 ? (
No comments yet.
) : (
{allComments.map((comment) => { const isSteering = isSteeringComment(comment); const canEdit = !isSteering && comment.author === currentAuthor; const isEditing = editingId === comment.id; return (
{isSteering ? ( AI Guidance ) : ( {comment.author} )} {isSteering ? formatRelativeTimestamp(comment.createdAt) : formatCommentTimestamp(comment)}
{canEdit && !isEditing ? (
) : null}
{isEditing ? (