import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, Bot, Check, ChevronDown, ChevronRight, Circle, CircleHelp, Clock3, ExternalLink, FileCode2, FileText, GitBranch, Loader2, PackageCheck, ShieldCheck, TerminalSquare, X, } from "lucide-react"; import { MarkdownBody } from "@/components/MarkdownBody"; import type { MentionOption } from "@/components/MarkdownEditor"; import { WorkspaceFileLink } from "@/components/WorkspaceFileLink"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { clearDraft, loadStructuredDraft, saveStructuredDraft, } from "@/lib/composer-draft"; import type { TaskChatProtocolItem, TaskChatProtocolStep, TaskChatProviderActivityItem, TaskChatRuntimeRequestDecision, TaskChatRuntimeRequestItem, TaskChatWorkspaceChangeItem, TaskChatWorkspaceFileItem, } from "./task-chat-model"; import { QuestionForm, QuestionResponseSummary } from "./QuestionForm"; import { TaskChatComposerTakeoverHeader } from "./TaskChatComposerTakeoverContext"; import { RichWorkProductCard } from "./RichWorkProductCard"; export interface TaskChatProtocolCardProps { item: TaskChatProtocolItem; presentation?: "timeline" | "takeover"; draftKey?: string; imageUploadHandler?: (file: File) => Promise; mentions?: MentionOption[]; onRuntimeRequestDecision?: ( item: TaskChatRuntimeRequestItem, decision: TaskChatRuntimeRequestDecision, ) => void | Promise; } const FAMILY_ICON = { plan: GitBranch, tool_execution: TerminalSquare, research: FileText, delegation: Bot, model_identity: Bot, context: PackageCheck, artifact: PackageCheck, review: ShieldCheck, hook: GitBranch, memory: FileText, safety: ShieldCheck, terminal: TerminalSquare, wait: Clock3, provider_notice: AlertTriangle, } as const; function statusTone(status: string): string { if ( ["failed", "blocked", "denied", "cancelled", "interrupted"].includes(status) ) return "text-destructive"; if (["running", "pending", "in_progress", "waiting"].includes(status)) return "text-(--status-agent-running)"; if (["completed", "done", "succeeded", "passed", "resolved"].includes(status)) return "text-(--status-task-icon-done)"; return "text-muted-foreground"; } function StatusIcon({ status, className, }: { status: string; className?: string; }) { if (["running", "pending", "in_progress", "waiting"].includes(status)) { return ( ); } if ( ["failed", "blocked", "denied", "cancelled", "interrupted"].includes(status) ) { return ( ); } if ( ["completed", "done", "succeeded", "passed", "resolved"].includes(status) ) { return ( ); } return ( ); } function CardShell({ icon: Icon, title, status, summary, children, testId, presentation = "timeline", }: { icon: typeof Circle; title: string; status: string | null; summary?: string; children?: React.ReactNode; testId: string; presentation?: "timeline" | "takeover"; }) { const titleRow = (
{title} {status && status !== "pending" ? ( {status.replaceAll("_", " ")} ) : null}
); if (presentation === "takeover") { return (
{titleRow} {summary ? (

{summary}

) : null} {children}
); } return (
{title} {status ? ( {status.replaceAll("_", " ")} ) : null}
{summary ? (

{summary}

) : null}
{children ? (
{children}
) : null}
); } function PlanSteps({ steps }: { steps: TaskChatProtocolStep[] }) { if (steps.length === 0) return null; return (
    {steps.map((step) => (
  1. {step.label}
  2. ))}
); } function ProviderActivityCard({ item, }: { item: TaskChatProviderActivityItem; }) { const Icon = FAMILY_ICON[item.family]; const hasDetails = item.details.length > 0 || item.links.length > 0 || item.children.length > 0 || Boolean(item.output); return ( {item.steps.length > 0 ? : 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.links.length > 0 ? (
    {item.links.map((link) => (
  • {link.label} {link.description ? (

    {link.description}

    ) : null}
  • ))}
) : null} {hasDetails ? (
0 || item.links.length > 0 || item.children.length > 0) && "mt-2", )} > Details {item.details.length > 0 ? (
{item.details.map((detail) => (
{detail.label}
{detail.value}
))}
) : null} {item.output ? (
              {item.output}
            
) : null} {item.outputTruncated ? (

Output truncated to 8 KiB.

) : null}
) : null}
); } function diffStats(file: TaskChatWorkspaceChangeItem["files"][number]) { return ( {file.additions == null ? "" : `+${file.additions}`} {file.additions != null && file.deletions != null ? " " : ""} {file.deletions == null ? "" : `−${file.deletions}`} ); } function WorkspaceChangeCard({ item }: { item: TaskChatWorkspaceChangeItem }) { const [selectedPath, setSelectedPath] = useState(null); const [expanded, setExpanded] = useState(false); const selected = item.files.find((file) => file.path === selectedPath) ?? null; const visibleFiles = expanded ? item.files : item.files.slice(0, 3); const fileCount = item.totals.files || item.files.length; const fileLabel = `${fileCount} ${fileCount === 1 ? "file" : "files"}`; const stats = [ item.totals.additions == null ? null : `+${item.totals.additions}`, item.totals.deletions == null ? null : `−${item.totals.deletions}`, ].filter(Boolean).join(" "); const summary = item.complete ? `${fileLabel} changed${stats ? ` · ${stats}` : ""}` : "Workspace changes in progress"; return ( <>
{visibleFiles.map((file) => ( ))}
{item.files.length > 3 ? ( ) : null} {item.files.length > 0 ? ( ) : null}
{ if (!open) setSelectedPath(null); }} > {selected?.path ?? "Workspace diff"} {item.files.length > 1 ? (
{item.files.map((file) => ( ))}
) : null} {selected?.binary ? (

Binary file; text diff is unavailable.

) : selected?.diff ? (
              {selected.diff}
            
) : (

No inline patch was recorded for this file.

)}
); } function WorkspaceFileCard({ item }: { item: TaskChatWorkspaceFileItem }) { const [open, setOpen] = useState(false); const extension = item.displayName.includes(".") ? item.displayName.split(".").at(-1)?.toUpperCase() : "FILE"; const label = item.presentation === "generic" ? "File" : titleCaseKey(item.presentation); const workspaceFileRef = useMemo( () => ({ path: item.path, resourceKind: "file" as const, line: item.line, column: null, raw: item.path, }), [item.line, item.path], ); return ( <>
{item.displayName}

{item.path}

{item.preview == null ? (

Preview unavailable. The verified workspace reference is still available above.

) : item.presentation === "document" ? (
{item.preview}
) : item.presentation === "image" && item.mediaType?.startsWith("image/") && item.preview.startsWith("data:") ? ( {item.displayName} ) : (
              {item.preview}
            
)} {item.previewTruncated ? (

Preview truncated by the runner.

) : null}
); } function titleCaseKey(value: string): string { return value .replaceAll("_", " ") .replace(/\b\w/g, (letter) => letter.toUpperCase()); } function RuntimeQuestionHistory({ item, }: { item: TaskChatRuntimeRequestItem; }) { const questionSet = item.questionSet; if (!questionSet) return null; return (
{item.response ? ( ) : (
{questionSet.questions.map((question) => (
{question.header ? (

{question.header}

) : null}

{question.prompt}

))}

{item.status === "resolved" ? "Answer details were not recorded by this older runtime." : `No answers were submitted; this request was ${item.status}.`}

)}
); } function RuntimeQuestionReceipt({ item, }: { item: TaskChatRuntimeRequestItem; }) { const label = item.status === "resolved" ? "Questions answered" : item.status === "cancelled" ? "Questions cancelled" : item.status === "expired" ? "Questions expired" : "Questions resolved"; return (
{label}

{item.questionSet?.title ?? "Runtime input"}

{item.prompt}

); } function RuntimeRequestCard({ item, onDecision, presentation, draftKey, imageUploadHandler, mentions, }: { item: TaskChatRuntimeRequestItem; onDecision?: TaskChatProtocolCardProps["onRuntimeRequestDecision"]; presentation: "timeline" | "takeover"; draftKey?: string; imageUploadHandler?: (file: File) => Promise; mentions?: MentionOption[]; }) { const title = item.questionSet?.title ?? (item.requestType === "permission" ? "Runtime permission" : "Runtime input"); const fields = item.fields.length > 0 ? item.fields : [{ name: "answer", label: "Response", placeholder: null }]; const [values, setValues] = useState>(() => draftKey ? loadStructuredDraft(draftKey, {}) : {}, ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!draftKey) setValues({}); setSubmitting(false); setError(null); }, [draftKey, item.requestId, item.status]); useEffect(() => { if (draftKey) saveStructuredDraft(draftKey, values); }, [draftKey, values]); if ( presentation === "timeline" && item.requestType === "input" && item.status !== "pending" && item.questionSet ) { return ; } const submit = async (decision: TaskChatRuntimeRequestDecision) => { if (!onDecision || item.status !== "pending" || submitting) return; setSubmitting(true); setError(null); try { await onDecision(item, decision); if (draftKey) clearDraft(draftKey); } catch (cause) { setSubmitting(false); setError( cause instanceof Error ? cause.message : "The runtime request could not be resolved.", ); } }; const canSubmitInput = fields.some( (field) => (values[field.name] ?? "").trim().length > 0, ); if ( presentation === "takeover" && item.requestType === "input" && item.status === "pending" && item.questionSet ) { return ( submit({ action: "submit", response })} /> ); } return ( {item.requestType === "input" && item.status === "pending" && item.questionSet ? ( submit({ action: "submit", response })} onCancel={ presentation === "timeline" ? () => submit({ action: "cancel" }) : undefined } /> ) : item.requestType === "input" && item.status !== "pending" && item.questionSet ? ( ) : item.requestType === "input" && item.status === "pending" ? (
{ event.preventDefault(); if (canSubmitInput) void submit({ action: "submit", values }); }} > {fields.map((field) => { const inputId = `${item.id}:${field.name}`; return (