import { useId, useState, type ReactNode } from "react"; import { Check, ChevronRight, Circle, ExternalLink, Loader2, X, } from "lucide-react"; import { MarkdownBody } from "@/components/MarkdownBody"; import { WorkspaceFileLink } from "@/components/WorkspaceFileLink"; import { useSecondTick } from "@/hooks/useSecondTick"; import { cn } from "@/lib/utils"; import { useTaskChatShimmer } from "./TaskChatShimmer"; import { AutoApprovedBadge, AutoReviewDetail } from "./TaskChatAutoReview"; import type { TaskChatProtocolDetail, TaskChatProtocolItem, TaskChatProtocolStep, TaskChatProviderActivityItem, TaskChatWorkspaceChangeItem, TaskChatWorkspaceFileItem, } from "./task-chat-model"; import { durationLabel, protocolActivityIsRunning, protocolActivityLabel, protocolActivityPresentation, providerNoticeSeverity, } from "./task-chat-activity-presentation"; const COMPACT_RESEARCH_RESULT_LIMIT = 5; const COMPACT_WORKSPACE_FILE_LIMIT = 8; const RESEARCH_SOURCE_PREVIEW_LIMIT = 3; function StatusIcon({ status }: { status: "running" | "completed" | "failed" | "interrupted" | "informational" }) { if (status === "running") return ; if (status === "failed" || status === "interrupted") return ; if (status === "completed") return ; return ; } function stepStatusIcon(status: TaskChatProtocolStep["status"], neutral = false) { if (neutral && (status === "blocked" || status === "failed")) return ; if (status === "in_progress") return ; if (status === "completed") return ; if (status === "blocked" || status === "failed") return ; return ; } export function DetailList({ details }: { details: readonly TaskChatProtocolDetail[] }) { if (details.length === 0) return null; return (
{details.map((detail) => (
{detail.label}
{detail.value}
))}
); } function sourceHostname(href: string): string { try { return new URL(href).hostname.replace(/^www\./, ""); } catch { return href; } } function ResearchDetails({ item }: { item: TaskChatProviderActivityItem }) { const [showAllResults, setShowAllResults] = useState(false); const query = item.details.find((detail) => detail.label.toLowerCase() === "query"); const additionalDetails = item.details.filter((detail) => !["action", "query", "status"].includes(detail.label.toLowerCase())); const visibleLinks = showAllResults ? item.links : item.links.slice(0, COMPACT_RESEARCH_RESULT_LIMIT); const hiddenResultCount = item.links.length - visibleLinks.length; return (
{query ? (

Query

{query.value}

) : null} {item.links.length > 0 ? (

{item.links.length} {item.links.length === 1 ? "result" : "results"}

    {visibleLinks.map((link, index) => (
  1. {link.label}

    {sourceHostname(link.href)} {link.description ? {link.description} : null}

  2. ))}
{hiddenResultCount > 0 ? ( ) : showAllResults && item.links.length > COMPACT_RESEARCH_RESULT_LIMIT ? ( ) : null}
) : null} {item.output || item.outputTruncated ? (

Additional provider output is available in Runner Inspector.

) : null}
); } function ProviderDetails({ item, neutral = false }: { item: TaskChatProviderActivityItem; neutral?: boolean }) { if (item.family === "research") return ; return (
{item.steps.length > 0 ? (
    {item.steps.map((step) => (
  1. {stepStatusIcon(step.status, neutral)} {step.label}
  2. ))}
) : null} {item.links.length > 0 ? (
    {item.links.map((link) => (
  • {link.label} {link.description ?

    {link.description}

    : null}
  • ))}
) : null} {item.children.length > 0 ? (
    {item.children.map((child) => (
  • {child.title} {child.status} {child.metadata ? {child.metadata} : null} {child.summary ? {child.summary} : null}
  • ))}
) : null} {item.details.length > 0 ? ( ) : null} {item.output ? (
{item.output}
) : null} {item.outputTruncated ?

Output truncated to 8 KiB.

: null}
); } function WorkspaceChangeDetails({ item }: { item: TaskChatWorkspaceChangeItem }) { if (item.files.length === 0) return

No changed-file details were reported.

; const visibleFiles = item.files.slice(0, COMPACT_WORKSPACE_FILE_LIMIT); const hiddenFileCount = item.files.length - visibleFiles.length; return (
    {visibleFiles.map((file) => (
  • {file.previousPath ? `${file.previousPath} → ${file.path}` : file.path} {file.operation.replaceAll("_", " ")} {file.additions != null || file.deletions != null ? ( {file.additions == null ? "" : `+${file.additions}`} {file.deletions == null ? "" : `−${file.deletions}`} ) : null}
  • ))}
{hiddenFileCount > 0 ?

{hiddenFileCount} more {hiddenFileCount === 1 ? "file" : "files"} not shown

: null}
); } function WorkspaceFileDetails({ item }: { item: TaskChatWorkspaceFileItem }) { const workspaceFileRef = { path: item.path, resourceKind: "file" as const, line: item.line, column: null, raw: item.path, }; return (
{item.preview ? ( item.presentation === "document" ? (
{item.preview}
) : (
{item.preview}
) ) :

Preview unavailable.

} {item.previewTruncated ?

Preview truncated by the runner.

: null}
); } /** One-line outcome for a check: its detail, or why it did not run. */ export function verificationOutcome(item: TaskChatProviderActivityItem): string | undefined { const detail = item.details.find((entry) => entry.label === "Detail")?.value; if (detail) return detail; const reason = item.details.find((entry) => entry.label === "Reason Code")?.value; return reason ? reason.replaceAll("_", " ").replace(/^\w/, (letter) => letter.toUpperCase()) : undefined; } /** The full check: command and untruncated outcome, wrapped. */ function VerificationDetails({ item }: { item: TaskChatProviderActivityItem }) { const outcome = verificationOutcome(item); return (
{item.summary ?

{item.summary}

: null} {outcome ?

{outcome}

: null}
); } function detailContent(item: TaskChatProtocolItem, neutral = false): ReactNode | null { switch (item.surface) { case "provider_activity": { if (item.family === "verification") return verificationOutcome(item) ? : null; const expandable = item.details.length > 0 || item.steps.length > 0 || item.links.length > 0 || item.children.length > 0 || Boolean(item.output) || Boolean(item.outputTruncated); return expandable ? : null; } case "workspace_change": return ; case "workspace_file": return ; case "resource": case "runtime_request": case "run_result": case "run_terminal": return null; } } function safeActivityHref(value: string | null | undefined): string | undefined { if (!value) return undefined; try { // A fixed HTTPS base accepts relative app paths and fragments as well as // external HTTP(S) resources, but never executable or local-file schemes. const url = new URL(value, "https://paperclip.invalid"); return url.protocol === "https:" || url.protocol === "http:" ? value : undefined; } catch { return undefined; } } export function hasTaskChatProtocolActivityDetails(item: TaskChatProtocolItem): boolean { if (item.surface === "provider_activity" && item.autoReview) return true; // A check expands only when it has an outcome that may not fit on one line. if (item.surface === "provider_activity" && item.family === "verification") return detailContent(item) !== null; if (item.surface === "resource") return Boolean(safeActivityHref(item.href)); return detailContent(item) !== null || (item.surface === "provider_activity" && Boolean(item.summary?.trim())); } export function TaskChatProtocolActivityDetails({ item, neutral = false }: { item: TaskChatProtocolItem; neutral?: boolean }) { const detail = detailContent(item, neutral); if (item.surface === "resource") { const href = safeActivityHref(item.href); return href ? {item.title} :

{item.title}

; } return <>{item.surface === "provider_activity" && item.family !== "verification" && item.summary ?

{item.summary}

: null}{detail}{item.surface === "provider_activity" && item.autoReview ? : null}; } /** Source domains under a finished web search, so results are visible without expanding. */ export function ResearchSourceStrip({ item }: { item: TaskChatProviderActivityItem }) { const seen = new Set(); const sources = item.links.flatMap((link) => { const host = sourceHostname(link.href); if (seen.has(host)) return []; seen.add(host); return [{ ...link, host }]; }); if (sources.length === 0) return null; const visible = sources.slice(0, RESEARCH_SOURCE_PREVIEW_LIMIT); const hidden = item.links.length - visible.length; return (
{visible.map((source) => ( {source.host} ))} {hidden > 0 ? +{hidden} more : null}
); } /** Live "12s left" for an intentional wait with a planned duration. */ export function WaitCountdown({ item }: { item: TaskChatProviderActivityItem }) { useSecondTick(true); const planned = Number(item.details.find((detail) => detail.label === "Planned Duration Ms")?.value); const startedAtMs = item.startedAt ? Date.parse(item.startedAt) : Number.NaN; if (!Number.isFinite(planned) || !Number.isFinite(startedAtMs)) return null; const remaining = Math.max(0, planned - (Date.now() - startedAtMs)); return {durationLabel(String(remaining))} left; } export function verificationTone(item: TaskChatProviderActivityItem): string | undefined { const status = item.details.find((detail) => detail.label === "Status")?.value; if (status === "passed") return "text-(--status-task-icon-done)"; if (status === "failed") return "text-destructive"; return undefined; } function itemStatus(item: TaskChatProtocolItem): "running" | "completed" | "failed" | "interrupted" | "informational" { if (item.surface === "provider_activity") return item.status; if (item.surface === "workspace_change") return item.complete ? "completed" : "running"; return "completed"; } export function TaskChatProtocolActivityRow({ item }: { item: TaskChatProtocolItem }) { const shimmer = useTaskChatShimmer(item.id); const [open, setOpen] = useState(false); const detailId = `task-chat-protocol-activity-${useId().replaceAll(":", "")}`; const presentation = protocolActivityPresentation(item); if (!presentation) return null; if (item.surface === "provider_activity" && item.family === "provider_notice") { const NoticeIcon = presentation.icon; const severity = providerNoticeSeverity(item); const summary = item.summary ?? item.details.find((entry) => entry.label === "Summary")?.value ?? "The provider reported a notice without a message."; return (
{severity === "error" ? "Error" : severity === "info" ? "Provider update" : "Warning"}

{summary}

{item.details.some(detail => detail.label !== "Summary") ? (
Details detail.label !== "Summary")} />
) : null}
); } const verification = item.surface === "provider_activity" && item.family === "verification" ? item : null; const runningWait = item.surface === "provider_activity" && item.family === "wait" && item.status === "running" ? item : null; const research = item.surface === "provider_activity" && item.family === "research" && item.status !== "running" ? item : null; const detail = item.surface === "provider_activity" && item.autoReview ? <>{detailContent(item)} : detailContent(item); const expandable = detail !== null; const Icon = presentation.icon; const label = protocolActivityLabel(item, presentation); const active = protocolActivityIsRunning(item); const status = itemStatus(item); const row = ( <> {label}{active ? "…" : ""} {presentation.detail ? {presentation.detail} : null} {verification && verificationOutcome(verification) ? ( {verificationOutcome(verification)} ) : null} {runningWait ? : null} {item.surface === "provider_activity" && item.autoReview ? : null} {expandable ? ( ) : null} {verification ? null : } ); const resourceHref = item.surface === "resource" ? safeActivityHref(item.href) : undefined; if (resourceHref) { return ( {row} ); } const activityRow = expandable ? ( ) : (
{row}
); return (
{activityRow} {presentation.note ? (

{presentation.note}

) : null} {research && !open ? : null} {open && detail ? (
{detail}
) : null}
); }