diff --git a/components/artifacts/demo-card.tsx b/components/artifacts/demo-card.tsx index 29b9441..3ab0d03 100644 --- a/components/artifacts/demo-card.tsx +++ b/components/artifacts/demo-card.tsx @@ -99,10 +99,10 @@ export function DemoOutputCard({ output }: { output: unknown }) { ); } - return ; + return ; } -function DemoFrame({ title, html, notes }: { title: string; html: string; notes?: string }) { +function DemoFrame({ title, html }: { title: string; html: string }) { const actions = useContext(ChatActionsContext); const iframeRef = useRef(null); const [state, setState] = useState(INITIAL_DEMO_BRIDGE_STATE); @@ -145,7 +145,6 @@ function DemoFrame({ title, html, notes }: { title: string; html: string; notes? style={{ height: state.height ?? 360 }} data-testid="demo-iframe" /> - {notes &&

{notes}

} ); } diff --git a/components/artifacts/steer-card.tsx b/components/artifacts/steer-card.tsx index bd7fe4c..d1b223d 100644 --- a/components/artifacts/steer-card.tsx +++ b/components/artifacts/steer-card.tsx @@ -1,6 +1,8 @@ "use client"; -import type { SubagentArtifactData, TeacherUIMessage } from "@/src/core"; +import { CheckIcon } from "lucide-react"; +import type { ResearchResult, SubagentArtifactData, TeacherUIMessage } from "@/src/core"; +import { isSubagentFailure } from "@/src/core"; import { DemoOutputCard } from "@/components/artifacts/demo-card"; import { ResearchBriefCard } from "@/components/artifacts/job-card"; import { VisualOutputCard } from "@/components/artifacts/visual-card"; @@ -10,10 +12,17 @@ import { VisualOutputCard } from "@/components/artifacts/visual-card"; * * 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 must NEVER render as a - * learner bubble: the model-facing text part is protocol glue, and the - * artifact itself renders here, by kind, reusing the same artifact cards - * the result always used. + * ("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" badge. The full brief is TOO LONG + * to inline and the teacher's wake turn weaves it into the lesson anyway; + * it stays persisted in the transcript (the artifact part), so a + * "view full research" affordance can be added later. 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: TeacherUIMessage }) { const artifactPart = message.parts.find((p) => p.type === "data-subagent-artifact"); @@ -27,10 +36,27 @@ export function SubagentSteerCard({ message }: { message: TeacherUIMessage }) { } switch (data.kind) { case "research": - return ; + return isSubagentFailure("research", data.result as ResearchResult) ? ( + + ) : ( + + ); case "demo": return ; case "visual": return ; } } + +/** Quiet one-liner, the dispatch-settled idiom: the brief lives in the record, + * the lesson lives in the teacher's wake turn — the badge just marks delivery. */ +function ResearchCompleteBadge({ result }: { result: ResearchResult }) { + return ( +
+ + Research + complete + {result.sources.length > 0 && · {result.sources.length} sources} +
+ ); +} diff --git a/components/artifacts/visual-card.tsx b/components/artifacts/visual-card.tsx index f96f9f5..ffed8d0 100644 --- a/components/artifacts/visual-card.tsx +++ b/components/artifacts/visual-card.tsx @@ -61,7 +61,6 @@ export function VisualOutputCard({ output }: { output: unknown }) { dangerouslySetInnerHTML={{ __html: sanitizeSvg(result.svg) }} data-testid="visual-svg" /> - {result.notes &&

{result.notes}

} ); } diff --git a/components/chat.tsx b/components/chat.tsx index 3dbdf3d..22d209d 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -287,21 +287,22 @@ export function Chat({ /> )} - {messages.map((m, mi) => ( + {messages.map((m, mi) => + m.role === "user" && isSubagentSteer(m) ? ( + // Sub-agent delivery (wake-pipeline steer): NO message bubble — the + // artifact card sits in the transcript column directly, full width + // and centered like the teacher's artifacts (contracts.ts "THE ASYNC + // PROTOCOL" §4). + + ) : ( {m.role === "user" ? ( - isSubagentSteer(m) ? ( - // Sub-agent delivery (wake-pipeline steer): render the - // artifact, never a learner bubble (contracts.ts §4). - - ) : ( - // User text renders plain: the model's LaTeX pipeline is not - // applied to learner-authored text ($ stays $, no surprises). -
- {m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")} -
- ) + // User text renders plain: the model's LaTeX pipeline is not + // applied to learner-authored text ($ stays $, no surprises). +
+ {m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")} +
) : ( m.parts.map((part, pi) => { const isLastPart = mi === messages.length - 1 && pi === m.parts.length - 1; diff --git a/tests/demos.test.tsx b/tests/demos.test.tsx index abaf1b7..fac56ef 100644 --- a/tests/demos.test.tsx +++ b/tests/demos.test.tsx @@ -329,24 +329,35 @@ describe("steer card rendering (the delivered artifact)", () => { assert.ok(!html.includes(" { + it("renders the verified svg inline (sanitized); the maker's notes are glue and stay hidden", () => { const svg = ''; - const steer = buildSubagentSteer({ id: "jv", kind: "visual", toolCallId: "cv" }, { svg, notes: "The unit circle." }); - const html = renderToStaticMarkup(); - assert.match(html, /The unit circle\./); - assert.ok(html.includes("circle"), "svg content rendered inline"); - assert.match(html, /Figure/); - }); - - it("renders the research brief with source links", () => { const steer = buildSubagentSteer( - { id: "jr", kind: "research", toolCallId: "cr" }, - { brief: "Plato (c. 428–348 BCE) founded the Academy.", sources: [{ title: "Britannica", url: "https://britannica.com/plato" }] }, + { id: "jv", kind: "visual", toolCallId: "cv" }, + { svg, notes: "Looking at the render, the diagram is clean and accurate." }, ); const html = renderToStaticMarkup(); - assert.match(html, /Research brief/); - assert.match(html, /britannica\.com\/plato/); - assert.match(html, /founded the Academy/); + assert.ok(html.includes("circle"), "svg content rendered inline"); + assert.match(html, /Figure/); + assert.ok(!html.includes("Looking at the render"), "model-facing notes never render"); + }); + + it("renders a quiet Research-complete badge — the full brief stays out of the transcript (but persisted)", () => { + const steer = buildSubagentSteer( + { id: "jr", kind: "research", toolCallId: "cr" }, + { + brief: "Plato (c. 428–348 BCE) founded the Academy, a very long brief that would clutter the transcript.", + sources: [ + { title: "Britannica", url: "https://britannica.com/plato" }, + { title: "IEP", url: "https://iep.utm.edu/plato/" }, + ], + }, + ); + const html = renderToStaticMarkup(); + assert.match(html, /Research/); + assert.match(html, /complete/); + assert.match(html, /2 sources/); + assert.ok(!html.includes("founded the Academy"), "the long brief never renders inline"); + assert.ok(!html.includes("britannica"), "source links stay out too — the wake turn cites them"); }); it("renders the visual failure note with no svg when the build FAILED", () => {