fix(pdf): render the pdf dispatch card (live browser-smoke gap)

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.
This commit is contained in:
amosblomqvist
2026-09-19 16:27:11 +03:00
parent 2780166904
commit dfe1a21989
5 changed files with 74 additions and 4 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import type { ArtifactPartProps } from "@/src/core"; import type { ArtifactPartProps } from "@/src/core";
import { DemoCard } from "@/components/artifacts/demo-card"; 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 { VisualCard } from "@/components/artifacts/visual-card";
import { ProbeDoneCard } from "@/components/artifacts/probe-done-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<string, ComponentType<ArtifactPartProps>> = { export const artifactRenderers: Record<string, ComponentType<ArtifactPartProps>> = {
research: JobCard, 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 // plan_* parts render nothing inline by design: the phase seam is a
// message-level boundary, drawn by PhaseDivider in chat.tsx (the old // message-level boundary, drawn by PhaseDivider in chat.tsx (the old
// part-level plan_init divider retired with it). // part-level plan_init divider retired with it).
+11
View File
@@ -18,6 +18,17 @@ export function JobCard({ part }: ArtifactPartProps) {
return <DispatchCard toolCallId={part.toolCallId} kind="research" />; return <DispatchCard toolCallId={part.toolCallId} kind="research" />;
} }
/**
* 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 <DispatchCard toolCallId={part.toolCallId} kind="pdf" />;
}
/** /**
* The PDF reading brief (sources v0) — markdown body + the consulted-pages * The PDF reading brief (sources v0) — markdown body + the consulted-pages
* line. Exported: the steer card renders it under the PDF-complete badge, * line. Exported: the steer card renders it under the PDF-complete badge,
+12 -2
View File
@@ -549,7 +549,14 @@ export function Chat({
(n, m) => (n, m) =>
n + n +
m.parts.filter( 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, ).length,
0, 0,
), ),
@@ -576,7 +583,10 @@ export function Chat({
for (const m of messages) { for (const m of messages) {
for (const p of m.parts) { for (const p of m.parts) {
if ( 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.toolCallId &&
p.state !== "input-streaming" && p.state !== "input-streaming" &&
p.state !== "output-error" p.state !== "output-error"
+4
View File
@@ -2,6 +2,7 @@ import type { UIMessage } from "ai";
import type { import type {
DemoToolInput, DemoToolInput,
GraphContextData, GraphContextData,
PdfToolInput,
PlanVerdictData, PlanVerdictData,
PlanBlockData, PlanBlockData,
ProbeDoneInput, ProbeDoneInput,
@@ -34,6 +35,9 @@ export type TeacherUITools = {
research: { input: ResearchToolInput; output: string }; research: { input: ResearchToolInput; output: string };
demo: { input: DemoToolInput; output: string }; demo: { input: DemoToolInput; output: string };
visual: { input: VisualToolInput; 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 = { export type TeacherUIDataParts = {
+45 -1
View File
@@ -40,7 +40,7 @@ import {
import { sanitizeSvg, VisualCard } from "../components/artifacts/visual-card.tsx"; import { sanitizeSvg, VisualCard } from "../components/artifacts/visual-card.tsx";
import { SubagentSteerCard } from "../components/artifacts/steer-card.tsx"; import { SubagentSteerCard } from "../components/artifacts/steer-card.tsx";
import { DispatchCard } from "../components/artifacts/dispatch-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 { JobsContext } from "../components/jobs-context.ts";
import { demoInputSchema, jobTools, pdfInputSchema, visualInputSchema } from "../src/agent/tools/jobs.ts"; import { demoInputSchema, jobTools, pdfInputSchema, visualInputSchema } from "../src/agent/tools/jobs.ts";
import { runDemoBuilder } from "../src/agent/subagents/demo-builder.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, ""); 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(
<JobsContext.Provider value={[pdfJob]}>
<PdfJobCard part={ackPart as never} />
</JobsContext.Provider>,
);
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(
<JobsContext.Provider value={[pdfJob]}>
<PdfJobCard part={ackPart as never} />
</JobsContext.Provider>,
);
assert.match(html, /PDF reader/);
assert.match(html, /delivered/);
});
}); });
describe("steer card rendering (the delivered artifact)", () => { 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", () => { it("the artifact renderer registry maps demo/visual to the W6 cards", () => {
assert.equal(artifactRenderers.demo, DemoCard); assert.equal(artifactRenderers.demo, DemoCard);
assert.equal(artifactRenderers.visual, VisualCard); assert.equal(artifactRenderers.visual, VisualCard);
// sources v0 — pdf maps to its dispatch card (the live-smoke gap)
assert.equal(artifactRenderers.pdf, PdfJobCard);
}); });
}); });