From dfe1a2198909b1af953e3c13bb6baaa1c11f6769 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Sat, 19 Sep 2026 16:27:11 +0300 Subject: [PATCH] fix(pdf): render the pdf dispatch card (live browser-smoke gap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser e2e check on the live dispatch session found the tool-pdf part rendered NO card: the artifact renderer registry had no pdf entry (the doc comment literally says "add your renderer here"), and the typed TeacherUITools union + the chat.tsx job-part enumerations (poll cadence, dispatch nudge, settle-sync) all missed the new tool kind. - PdfJobCard in job-card.tsx: the research JobCard idiom, kind pdf — registered in artifacts/index.ts. - ui-message.ts: pdf entry in TeacherUITools (fire-and-forget ack output). - chat.tsx: two mechanical "tool-pdf" additions to the research/demo/visual enumerations — UI-stream file, flagged for review at merge (same treatment as the activity-dock type-forced entries). - demos.test.tsx: live/settled dispatch-card renders + registry pin. --- components/artifacts/index.ts | 3 +- components/artifacts/job-card.tsx | 11 ++++++++ components/chat.tsx | 14 ++++++++-- src/core/ui-message.ts | 4 +++ tests/demos.test.tsx | 46 ++++++++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/components/artifacts/index.ts b/components/artifacts/index.ts index 8b3b2d9..42b6871 100644 --- a/components/artifacts/index.ts +++ b/components/artifacts/index.ts @@ -1,7 +1,7 @@ import type { ComponentType } from "react"; import type { ArtifactPartProps } from "@/src/core"; import { DemoCard } from "@/components/artifacts/demo-card"; -import { JobCard } from "@/components/artifacts/job-card"; +import { JobCard, PdfJobCard } from "@/components/artifacts/job-card"; import { VisualCard } from "@/components/artifacts/visual-card"; import { ProbeDoneCard } from "@/components/artifacts/probe-done-card"; @@ -19,6 +19,7 @@ import { ProbeDoneCard } from "@/components/artifacts/probe-done-card"; */ export const artifactRenderers: Record> = { research: JobCard, + pdf: PdfJobCard, // sources v0 — live smoke 2026-09-19 caught the gap: without this the tool-pdf dispatch part rendered nothing // plan_* parts render nothing inline by design: the phase seam is a // message-level boundary, drawn by PhaseDivider in chat.tsx (the old // part-level plan_init divider retired with it). diff --git a/components/artifacts/job-card.tsx b/components/artifacts/job-card.tsx index 9047d92..e134fb1 100644 --- a/components/artifacts/job-card.tsx +++ b/components/artifacts/job-card.tsx @@ -18,6 +18,17 @@ export function JobCard({ part }: ArtifactPartProps) { return ; } +/** + * The pdf dispatch box (sources v0) — the same async-protocol idiom as + * JobCard: the ack output drives a DispatchCard (live "PDF reader" status + * via the central jobs poll); the brief itself rides the steer message + * and renders under the PDF-complete badge in the transcript. + */ +export function PdfJobCard({ part }: ArtifactPartProps) { + if (part.state !== "output-available") return null; + return ; +} + /** * The PDF reading brief (sources v0) — markdown body + the consulted-pages * line. Exported: the steer card renders it under the PDF-complete badge, diff --git a/components/chat.tsx b/components/chat.tsx index 9c4ee9e..8942db2 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -549,7 +549,14 @@ export function Chat({ (n, m) => n + m.parts.filter( - (p) => p.type === "tool-research" || p.type === "tool-demo" || p.type === "tool-visual", + // sources v0 — "tool-pdf" added to the job-part enumerations + // (mechanical, UI-stream review at merge): poll cadence + + // dispatch nudge + settle-sync all key on these parts. + (p) => + p.type === "tool-research" || + p.type === "tool-demo" || + p.type === "tool-visual" || + p.type === "tool-pdf", ).length, 0, ), @@ -576,7 +583,10 @@ export function Chat({ for (const m of messages) { for (const p of m.parts) { if ( - (p.type === "tool-research" || p.type === "tool-demo" || p.type === "tool-visual") && + (p.type === "tool-research" || + p.type === "tool-demo" || + p.type === "tool-visual" || + p.type === "tool-pdf") && // sources v0 — see the poll-cadence enumeration above p.toolCallId && p.state !== "input-streaming" && p.state !== "output-error" diff --git a/src/core/ui-message.ts b/src/core/ui-message.ts index 4c062bf..0bd5750 100644 --- a/src/core/ui-message.ts +++ b/src/core/ui-message.ts @@ -2,6 +2,7 @@ import type { UIMessage } from "ai"; import type { DemoToolInput, GraphContextData, + PdfToolInput, PlanVerdictData, PlanBlockData, ProbeDoneInput, @@ -34,6 +35,9 @@ export type TeacherUITools = { research: { input: ResearchToolInput; output: string }; demo: { input: DemoToolInput; output: string }; visual: { input: VisualToolInput; output: string }; + /** The pdf dispatch tool (sources v0): fire-and-forget ack output like + * research — the brief arrives later as a steer message. */ + pdf: { input: PdfToolInput; output: string }; }; export type TeacherUIDataParts = { diff --git a/tests/demos.test.tsx b/tests/demos.test.tsx index 3744c05..89637ec 100644 --- a/tests/demos.test.tsx +++ b/tests/demos.test.tsx @@ -40,7 +40,7 @@ import { import { sanitizeSvg, VisualCard } from "../components/artifacts/visual-card.tsx"; import { SubagentSteerCard } from "../components/artifacts/steer-card.tsx"; import { DispatchCard } from "../components/artifacts/dispatch-card.tsx"; -import { JobCard } from "../components/artifacts/job-card.tsx"; +import { JobCard, PdfJobCard } from "../components/artifacts/job-card.tsx"; import { JobsContext } from "../components/jobs-context.ts"; import { demoInputSchema, jobTools, pdfInputSchema, visualInputSchema } from "../src/agent/tools/jobs.ts"; import { runDemoBuilder } from "../src/agent/subagents/demo-builder.ts"; @@ -289,6 +289,48 @@ describe("dispatch card rendering (the tool-call ack box)", () => { ); assert.equal(html, ""); }); + + // sources v0 — the live browser smoke (2026-09-19) caught the pdf dispatch + // part rendering nothing: PdfJobCard is the fix. + it("pdf dispatch renders the live PDF-reader card while the job runs", () => { + const pdfJob: JobRecord = ({ + id: "jp1", + kind: "pdf", + chatId: "c", + toolCallId: "call-1", + status: "running", + activity: 'pdf_render: 42-44 @150dpi', + createdAt: new Date(Date.now() - 5000).toISOString(), + updatedAt: new Date().toISOString(), + }) as JobRecord; + const html = renderToStaticMarkup( + + + , + ); + assert.match(html, /PDF reader/); + assert.match(html, /pdf_render/); + }); + + it("pdf dispatch settles to the delivered one-liner", () => { + const pdfJob: JobRecord = ({ + id: "jp1", + kind: "pdf", + chatId: "c", + toolCallId: "call-1", + status: "done", + activity: "delivered", + createdAt: new Date(Date.now() - 5000).toISOString(), + updatedAt: new Date().toISOString(), + }) as JobRecord; + const html = renderToStaticMarkup( + + + , + ); + assert.match(html, /PDF reader/); + assert.match(html, /delivered/); + }); }); describe("steer card rendering (the delivered artifact)", () => { @@ -802,5 +844,7 @@ describe("tool + renderer registry wiring", () => { it("the artifact renderer registry maps demo/visual to the W6 cards", () => { assert.equal(artifactRenderers.demo, DemoCard); assert.equal(artifactRenderers.visual, VisualCard); + // sources v0 — pdf maps to its dispatch card (the live-smoke gap) + assert.equal(artifactRenderers.pdf, PdfJobCard); }); });