import { useMemo, useState } from "react"; import type { Task, TaskComment } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import "./TaskComments.css"; import { addSteeringComment, updateTaskComment, deleteTaskComment } from "../api"; import type { ToastType } from "../hooks/useToast"; const MAX_COMMENT_LENGTH = 2000; interface TaskCommentsProps { task: Task; onTaskUpdated?: (task: Task) => void; addToast: (message: string, type?: ToastType) => void; currentAuthor?: string; projectId?: 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 isAIGuidanceComment(author: string): boolean { return author === "agent" || author === "system"; } export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user", projectId }: TaskCommentsProps) { const [draft, setDraft] = useState(""); const [editingId, setEditingId] = useState(null); const [editingText, setEditingText] = useState(""); const [submitting, setSubmitting] = useState(false); const [deletingId, setDeletingId] = useState(null); // Sort comments by createdAt descending (newest first) const comments = useMemo(() => { return [...(task.comments || [])].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); }, [task.comments]); const isOverLimit = draft.length > MAX_COMMENT_LENGTH; async function handleAddComment() { const text = draft.trim(); if (!text) return; setSubmitting(true); try { const updated = await addSteeringComment(task.id, text, projectId); setDraft(""); onTaskUpdated?.(updated); addToast("Comment added", "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to add comment", "error"); } finally { setSubmitting(false); } } async function handleSaveEdit(commentId: string) { const text = editingText.trim(); if (!text) return; setSubmitting(true); try { const updated = await updateTaskComment(task.id, commentId, text, projectId); setEditingId(null); setEditingText(""); onTaskUpdated?.(updated); addToast("Comment updated", "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to update comment", "error"); } finally { setSubmitting(false); } } async function handleDelete(commentId: string) { setDeletingId(commentId); try { const updated = await deleteTaskComment(task.id, commentId, projectId); onTaskUpdated?.(updated); addToast("Comment deleted", "success"); } catch (error) { addToast(getErrorMessage(error) || "Failed to delete comment", "error"); } finally { setDeletingId(null); } } function handleKeyDown(event: React.KeyboardEvent) { if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { event.preventDefault(); void handleAddComment(); } } const placeholder = "Add a comment"; const buttonLabel = "Add Comment"; return (

Comments

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