import "./TaskReviewTab.css"; import type { Task, TaskDetail } from "@fusion/core"; import { useEffect, useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Components } from "react-markdown"; import { fetchTaskReview, refreshTaskReview, reviseTaskReviewItems } from "../api"; import type { SelectedReviewItem } from "../api"; import type { ToastType } from "../hooks/useToast"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; interface Props { task: Task | TaskDetail; projectId?: string; onTaskUpdated?: (task: Task) => void; addToast: (message: string, type?: ToastType) => void; } const REVIEW_LOAD_ERROR_MESSAGE = "Failed to load review data."; const DIRECT_MODE_EMPTY_MESSAGE = "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode."; const REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY = "fn-task-review-markdown"; type ReviewState = NonNullable; type ReviewItem = ReviewState["items"][number]; type AddressingRecord = ReviewState["addressing"][number]; type DisplayReviewItem = { id: string; summary: string; body: string; path?: string; createdAt?: string; status: "queued" | "in-progress" | "addressed" | "failed"; addressing?: AddressingRecord; item?: ReviewItem; }; function readBooleanPref(key: string, defaultValue: boolean): boolean { if (typeof window === "undefined") return defaultValue; try { const raw = window.localStorage.getItem(key); if (raw === null) return defaultValue; return raw === "true"; } catch { return defaultValue; } } function writeBooleanPref(key: string, value: boolean): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(key, value ? "true" : "false"); } catch { // ignore storage failures (quota, private mode, etc.) } } const markdownComponents: Components = { p: ({ children, ...props }) =>

{linkifyReactChildren(children)}

, li: ({ children, ...props }) =>
  • {linkifyReactChildren(children)}
  • , code: ({ children, ...props }) => {linkifyReactChildren(children)}, pre: ({ children, ...props }) => (
          {linkifyReactChildren(children)}
        
    ), table: ({ children, ...props }) => ( {children}
    ), }; function formatTimestamp(value?: string): string { if (!value) return "Never"; return new Date(value).toLocaleString(); } function formatRefreshSource(source?: "manual" | "auto" | "initial-load"): string { if (source === "manual") return "Manual"; if (source === "auto") return "Background"; return "Initial load"; } function getDisplayReviewItems(review: ReviewState): DisplayReviewItem[] { const addressingById = new Map(review.addressing.map((record) => [record.itemId, record] as const)); const items = review.items.map((item) => { const addressing = addressingById.get(item.id); return { id: item.id, summary: item.summary ?? item.body.slice(0, 120), body: item.body, path: item.path, createdAt: item.createdAt, status: addressing?.status ?? "queued", addressing, item, } satisfies DisplayReviewItem; }); const existingIds = new Set(items.map((item) => item.id)); const snapshots = review.addressing .filter((record) => !existingIds.has(record.itemId) && record.snapshot) .map((record) => ({ id: record.itemId, summary: record.snapshot?.summary ?? record.itemId, body: record.snapshot?.body ?? record.snapshot?.summary ?? record.itemId, path: record.snapshot?.filePath, createdAt: record.selectedAt, status: record.status, addressing: record, } satisfies DisplayReviewItem)); return [...items, ...snapshots]; } export function TaskReviewTab({ task, projectId, onTaskUpdated, addToast }: Props) { const [selected, setSelected] = useState([]); const [refreshing, setRefreshing] = useState(false); const [revising, setRevising] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [emptyMessage, setEmptyMessage] = useState(null); const [review, setReview] = useState(task.reviewState ?? null); const [renderMarkdown, setRenderMarkdown] = useState(() => readBooleanPref(REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY, true)); const canRevise = selected.length > 0 && !revising; const isPrMode = review?.source === "pull-request"; const displayItems = useMemo(() => (review ? getDisplayReviewItems(review) : []), [review]); useEffect(() => { writeBooleanPref(REVIEW_MARKDOWN_TOGGLE_STORAGE_KEY, renderMarkdown); }, [renderMarkdown]); useEffect(() => { let cancelled = false; setLoading(true); setError(null); void fetchTaskReview(task.id, projectId) .then((result) => { if (cancelled) return; setReview(result.reviewState); setEmptyMessage(result.emptyMessage ?? null); }) .catch(() => { if (cancelled) return; setError(REVIEW_LOAD_ERROR_MESSAGE); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [task.id, projectId]); const summaryText = useMemo(() => { if (!review) return "No review feedback captured yet."; if (review.source === "pull-request") { const prSummary = review.summary as { reviewDecision?: string } | undefined; return `${prSummary?.reviewDecision ?? "REVIEW_REQUIRED"} · ${displayItems.length} review item(s)`; } const reviewerSummary = review.summary as { summary?: string } | undefined; return `${reviewerSummary?.summary ?? "reviewer-agent"} · ${displayItems.length} review item(s)`; }, [review, displayItems.length]); const decisionLabel = !review ? undefined : review.source === "pull-request" ? (review.summary as { reviewDecision?: string } | undefined)?.reviewDecision : (review.summary as { verdict?: string } | undefined)?.verdict; const refreshStatus = refreshing ? "refreshing" : (review?.refreshStatus ?? "ready"); const refreshToneClass = refreshStatus === "error" ? "status-dot status-dot--error" : refreshStatus === "refreshing" ? "status-dot status-dot--pending" : "status-dot status-dot--online"; const toggleSelected = (id: string) => setSelected((prev) => (prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id])); const onRefresh = async () => { try { setError(null); setRefreshing(true); const result = await refreshTaskReview(task.id, projectId); setReview(result.reviewState); onTaskUpdated?.({ ...task, reviewState: result.reviewState, prInfo: result.prInfo ?? task.prInfo } as Task); if (result.reviewState.refreshStatus === "error") { const refreshMessage = result.reviewState.refreshError ?? "Failed to refresh review data."; setError(refreshMessage); addToast(refreshMessage, "error"); return; } addToast("Review refreshed", "success"); } catch (refreshError) { const message = refreshError instanceof Error ? refreshError.message : REVIEW_LOAD_ERROR_MESSAGE; setError(message); addToast(message, "error"); } finally { setRefreshing(false); } }; const onRevise = async () => { try { if (!review) return; setError(null); setRevising(true); const selectedItems: SelectedReviewItem[] = displayItems .filter((item) => selected.includes(item.id)) .map((item) => { if (!item.item) { return { id: item.id, source: review.source === "pull-request" ? "pr-review" : "reviewer-agent", threadId: item.addressing?.snapshot?.threadId, filePath: item.addressing?.snapshot?.filePath, lineNumber: item.addressing?.snapshot?.lineNumber, author: item.addressing?.snapshot?.authorLogin, summary: item.summary, body: item.body, url: item.addressing?.snapshot?.url, }; } const itemRecord = item.item as unknown as Record; return { id: item.item.id, source: review.source === "pull-request" ? "pr-review" : "reviewer-agent", threadId: typeof itemRecord.threadId === "string" ? itemRecord.threadId : undefined, filePath: item.item.path, lineNumber: typeof itemRecord.line === "number" ? itemRecord.line : undefined, author: item.item.author?.login, summary: item.item.summary ?? item.item.body.slice(0, 120), body: item.item.body, url: typeof itemRecord.url === "string" ? itemRecord.url : undefined, }; }); const result = await reviseTaskReviewItems(task.id, selectedItems, projectId); setReview(result.reviewState); onTaskUpdated?.({ ...result.task, reviewState: result.reviewState } as Task); setSelected([]); addToast("Same-task AI revision started from selected review feedback", "success"); } catch (reviseError) { const message = reviseError instanceof Error ? reviseError.message : "Failed to queue revision"; setError(message); addToast(message, "error"); } finally { setRevising(false); } }; return (

    {summaryText}

    {decisionLabel ? {decisionLabel} : null}
    {loading ?
    Loading review data…
    : null} {!loading && error ?
    {error}
    : null} {!loading && !error && !isPrMode && displayItems.length === 0 ?
    {emptyMessage ?? DIRECT_MODE_EMPTY_MESSAGE}
    : null} {!loading && !error && displayItems.length > 0 ? (
      {displayItems.map((item) => { const checkboxId = `task-review-item-checkbox-${item.id}`; return (
    • {formatTimestamp(item.createdAt)}
      {item.addressing ? (
      Selected: {formatTimestamp(item.addressing.selectedAt)}{item.addressing.startedAt ? ` · Started: ${formatTimestamp(item.addressing.startedAt)}` : ""}{item.addressing.completedAt ? ` · Completed: ${formatTimestamp(item.addressing.completedAt)}` : ""}{item.addressing.error ? ` · Error: ${item.addressing.error}` : ""}
      ) : null} {renderMarkdown ? (
      {item.body}
      ) : (
      {linkifyFilePaths(item.body)}
      )}
    • ); })}
    ) : null} {isPrMode && !loading && !error && displayItems.length === 0 ?
    No review items yet.
    : null}
    ); }