"use client"; import { useI18n } from "@/components/i18n"; import { CheckIcon, CircleX } from "lucide-react"; import type { PdfResult, ResearchResult, SubagentArtifactData } from "@/src/core"; import type { LhViewMessage } from "@/src/core"; import { isSubagentFailure } from "@/src/core"; import { DemoOutputCard } from "@/components/artifacts/demo-card"; import { PdfBriefCard, ResearchBriefCard } from "@/components/artifacts/job-card"; import { VisualOutputCard } from "@/components/artifacts/visual-card"; /** * The sub-agent result delivery card (contracts.ts "THE ASYNC PROTOCOL" §4). * * When a background job completes, the wake pipeline injects a STEER message * into the transcript: a user-role UIMessage whose data part * ("data-subagent-artifact") carries the result. It renders as an artifact — * NEVER a learner bubble, never wrapped in a message container: * * research — a quiet "Research complete" one-liner that expands on demand * (native
) to the full persisted brief + sources: the brief is * too long to inline by default and the teacher's wake turn weaves it * into the lesson anyway. Failures keep the failure card (the brief * explains what went wrong). * demo / visual — the artifact itself (sandboxed iframe / sanitized SVG). * The sub-agent's verification notes are model-facing glue and do NOT * render; only a failure note does. */ export function SubagentSteerCard({ message }: { message: LhViewMessage }) { const { t } = useI18n(); const artifactPart = message.parts.find((p) => p.type === "data-subagent-artifact"); const data = artifactPart?.data as SubagentArtifactData | undefined; // The abort steer (2026-09 dock semantics): its message-type part is // data-subagent-aborted (the custom-message scheme — the part, not // metadata, is the marker) — a quiet chip, mirroring the complete-badge. if (!data) { const aborted = message.parts.find((p) => p.type === "data-subagent-aborted") as | { data?: { kind?: string } } | undefined; if (aborted?.data) return ; return (
{t("A sub-agent finished, but its result could not be displayed.")}
); } switch (data.kind) { case "research": return isSubagentFailure("research", data.result as ResearchResult) ? ( ) : ( ); case "pdf": return isSubagentFailure("pdf", data.result as PdfResult) ? ( ) : ( ); case "demo": return ; case "visual": return ; } } /** The aborted-job chip: mirrors the complete-badge one-liner — the * teacher's wake turn acknowledges the abort in the lesson itself, * this just marks what happened. */ function AbortedBadge({ kind }: { kind: string }) { const { t } = useI18n(); return (
{kind} {t("aborted by the learner")}
); } /** Quiet one-liner, the research-badge idiom ported to PDF: the brief is * too long to inline by default and the teacher's wake turn weaves it in; * the badge marks delivery, the collapsed details carries the full text * + consulted pages. */ function PdfCompleteBadge({ result }: { result: PdfResult }) { const { t } = useI18n(); // The honest count is READ pages (extract/render — sources v1 split); // the ?? is runtime tolerance for pre-split persisted steers. const readPages = result.pagesRead ?? result.pagesConsulted; return (
PDF {t("reading complete")} {readPages.length > 0 && ( {t("· read {count} pages", { count: readPages.length })} )} {t("view brief")} {t("hide brief")}
); } /** Quiet one-liner, the dispatch-settled idiom — the lesson lives in the * teacher's wake turn; the badge just marks delivery. The persisted brief * rides along in a collapsed
(the wave-1 "view full research" * affordance): full markdown + sources, one click away, no extra fetch — * the artifact part in the record is the single source of truth. */ function ResearchCompleteBadge({ result }: { result: ResearchResult }) { const { t } = useI18n(); return (
{t("Research")} {t("complete")} {result.sources.length > 0 && · {t("{count} sources", { count: result.sources.length })}} {t("view brief")} {t("hide brief")}
); }