From 849a322c87b02a082a8d33867158c7a0493b01e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 14 Apr 2026 16:03:19 +0000 Subject: [PATCH] fix(chat): Validate analysis breakdown before render Malformed or incomplete generateAnalysisBreakdown output can sync through the\nchat store and reach the renderer before it is safe to display.\n\nValidate the breakdown payload before treating the tool as complete or\nrendering the breakdown card, and keep the breakdown hidden when the\npaired performance result reports no renderable data.\n\nFixes WEBVITALS-3M\nCo-Authored-By: Claude Co-authored-by: Sergiy Dybskiy --- components/MessageRenderer.tsx | 61 +++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/components/MessageRenderer.tsx b/components/MessageRenderer.tsx index 99056f2..74ff882 100644 --- a/components/MessageRenderer.tsx +++ b/components/MessageRenderer.tsx @@ -15,7 +15,10 @@ import { import { useWebVitalsScore } from "@/contexts/WebVitalsScoreContext"; import { calculateLighthouseScore } from "@/lib/web-vitals"; -import type { AnalysisBreakdown } from "@/types/analysis-breakdown"; +import { + AnalysisBreakdownSchema, + type AnalysisBreakdown, +} from "@/types/analysis-breakdown"; import type { LighthouseScoreData, RealWorldPerformanceOutput, @@ -51,6 +54,23 @@ interface MessageRendererProps { message: UIMessage; } +function isAnalysisBreakdownOutput( + output: unknown, +): output is AnalysisBreakdown { + return AnalysisBreakdownSchema.safeParse(output).success; +} + +function isRealWorldPerformanceOutput( + output: unknown, +): output is RealWorldPerformanceOutput { + return ( + typeof output === "object" && + output !== null && + "hasData" in output && + typeof output.hasData === "boolean" + ); +} + const MessageRenderer = memo(function MessageRenderer({ message, }: MessageRendererProps) { @@ -135,6 +155,27 @@ const MessageRenderer = memo(function MessageRenderer({ messages.findIndex((m) => m.role === "user") === messages.findIndex((m) => m.id === message.id); + const hasCompletedPerformanceResult = useMemo( + () => + performanceParts.some( + (part) => + part.state === "output-available" && + isRealWorldPerformanceOutput(part.output), + ), + [performanceParts], + ); + + const hasRenderablePerformanceData = useMemo( + () => + performanceParts.some( + (part) => + part.state === "output-available" && + isRealWorldPerformanceOutput(part.output) && + part.output.hasData, + ), + [performanceParts], + ); + // Extract domain from first user message const getDomainFromFirstMessage = () => { if (!isFirstUserMessage) return null; @@ -155,9 +196,17 @@ const MessageRenderer = memo(function MessageRenderer({ // Check if all tools have completed successfully const allToolsComplete = useMemo(() => { if (toolParts.length < 3) return false; // Wait for all 3 tools before hiding - return toolParts.every( - (tool) => tool.state === "output-available" && !tool.errorText, - ); + return toolParts.every((tool) => { + if (tool.state !== "output-available" || tool.errorText) { + return false; + } + + if (tool.type === "tool-generateAnalysisBreakdown") { + return isAnalysisBreakdownOutput(tool.output); + } + + return true; + }); }, [toolParts]); // Show tools during loading or if there are errors @@ -268,7 +317,9 @@ const MessageRenderer = memo(function MessageRenderer({ {analysisBreakdownParts.map((breakdownTool, i) => { if ( breakdownTool.state === "output-available" && - breakdownTool.output + breakdownTool.output && + isAnalysisBreakdownOutput(breakdownTool.output) && + (!hasCompletedPerformanceResult || hasRenderablePerformanceData) ) { return (