11import { useEffect , useMemo , useState } from "react" ;
22import {
33 AlertCircle ,
4+ AlertTriangle ,
45 CheckCircle2 ,
56 ChevronDown ,
67 ChevronRight ,
@@ -9,6 +10,8 @@ import {
910 Loader2 ,
1011 Pencil ,
1112 RotateCcw ,
13+ ShieldAlert ,
14+ ShieldCheck ,
1215} from "lucide-react" ;
1316import { Alert , AlertDescription } from "@mcpjam/design-system/alert" ;
1417import { Badge } from "@mcpjam/design-system/badge" ;
@@ -29,10 +32,17 @@ import {
2932 getXAAStepIndex ,
3033 XAA_STEP_ORDER ,
3134} from "@/lib/xaa/step-metadata" ;
35+ import type { XAAFlowState , XAAFlowStep } from "@/lib/xaa/types" ;
36+ import {
37+ getXAAErrorGuidance ,
38+ latestErroredHttpEntry ,
39+ type XAAErrorAction ,
40+ type XAAErrorGuidance ,
41+ } from "@/lib/xaa/error-guidance" ;
3242import type {
33- XAAFlowState ,
34- XAAFlowStep ,
35- } from "@/lib/xaa/types " ;
43+ XAACheckStatus ,
44+ XAACompatibilityReport ,
45+ } from "@/lib/xaa/capability-preflight " ;
3646import {
3747 NEGATIVE_TEST_MODES ,
3848 NEGATIVE_TEST_MODE_DETAILS ,
@@ -63,6 +73,176 @@ interface XAAFlowLoggerProps {
6373 } ;
6474}
6575
76+ function CompatibilityBanner ( {
77+ report,
78+ } : {
79+ report : XAACompatibilityReport ;
80+ } ) {
81+ const [ expanded , setExpanded ] = useState ( report . overall !== "pass" ) ;
82+ useEffect ( ( ) => {
83+ setExpanded ( report . overall !== "pass" ) ;
84+ } , [ report . overall ] ) ;
85+
86+ const tone =
87+ report . overall === "pass"
88+ ? {
89+ Icon : ShieldCheck ,
90+ iconClass : "text-green-600 dark:text-green-400" ,
91+ borderClass : "border-green-500/40" ,
92+ bgClass : "bg-green-500/5" ,
93+ title : "Authorization server looks XAA-ready" ,
94+ }
95+ : report . overall === "warn"
96+ ? {
97+ Icon : AlertTriangle ,
98+ iconClass : "text-amber-500" ,
99+ borderClass : "border-amber-500/40" ,
100+ bgClass : "bg-amber-500/5" ,
101+ title : "Authorization server capabilities are ambiguous" ,
102+ }
103+ : {
104+ Icon : ShieldAlert ,
105+ iconClass : "text-red-500" ,
106+ borderClass : "border-red-500/40" ,
107+ bgClass : "bg-red-500/5" ,
108+ title : "Authorization server isn't XAA-ready" ,
109+ } ;
110+
111+ const checkStatusClass = ( status : XAACheckStatus ) =>
112+ status === "pass"
113+ ? "text-green-600 dark:text-green-400"
114+ : status === "fail"
115+ ? "text-red-500"
116+ : "text-amber-500" ;
117+
118+ const checkStatusSymbol = ( status : XAACheckStatus ) =>
119+ status === "pass" ? "✓" : status === "fail" ? "✗" : "?" ;
120+
121+ return (
122+ < div
123+ className = { cn (
124+ "rounded-md border px-3 py-2.5 text-xs" ,
125+ tone . borderClass ,
126+ tone . bgClass ,
127+ ) }
128+ >
129+ < button
130+ type = "button"
131+ onClick = { ( ) => setExpanded ( ( prev ) => ! prev ) }
132+ className = "flex w-full items-start gap-2 text-left"
133+ >
134+ < tone . Icon className = { cn ( "h-4 w-4 mt-0.5 shrink-0" , tone . iconClass ) } />
135+ < div className = "flex-1 min-w-0 space-y-1" >
136+ < div className = "font-medium text-foreground" > { tone . title } </ div >
137+ { report . vendorHint && (
138+ < div className = "text-muted-foreground" >
139+ { report . vendorHint . note }
140+ </ div >
141+ ) }
142+ </ div >
143+ { expanded ? (
144+ < ChevronDown className = "h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
145+ ) : (
146+ < ChevronRight className = "h-3.5 w-3.5 mt-1 text-muted-foreground shrink-0" />
147+ ) }
148+ </ button >
149+ { expanded && (
150+ < ul className = "mt-2 space-y-1 border-t border-border/50 pt-2" >
151+ { report . checks . map ( ( check ) => (
152+ < li key = { check . id } className = "flex items-start gap-2" >
153+ < span
154+ className = { cn (
155+ "font-mono shrink-0 w-3" ,
156+ checkStatusClass ( check . status ) ,
157+ ) }
158+ aria-hidden
159+ >
160+ { checkStatusSymbol ( check . status ) }
161+ </ span >
162+ < div className = "min-w-0 flex-1" >
163+ < span className = "font-medium text-foreground" >
164+ { check . label }
165+ </ span >
166+ < span className = "text-muted-foreground" > — { check . detail } </ span >
167+ </ div >
168+ </ li >
169+ ) ) }
170+ </ ul >
171+ ) }
172+ </ div >
173+ ) ;
174+ }
175+
176+ function GuidanceCallout ( {
177+ guidance,
178+ onConfigure,
179+ onShowBootstrap,
180+ onReset,
181+ } : {
182+ guidance : XAAErrorGuidance ;
183+ onConfigure ?: ( ) => void ;
184+ onShowBootstrap ?: ( ) => void ;
185+ onReset ?: ( ) => void ;
186+ } ) {
187+ const toneClass =
188+ guidance . severity === "error"
189+ ? "border-red-500/40 bg-red-500/5"
190+ : "border-amber-500/40 bg-amber-500/5" ;
191+ const iconClass =
192+ guidance . severity === "error" ? "text-red-500" : "text-amber-500" ;
193+
194+ const handleAction = ( action : XAAErrorAction ) => {
195+ if ( action . intent === "configure" ) onConfigure ?.( ) ;
196+ else if ( action . intent === "bootstrap" ) onShowBootstrap ?.( ) ;
197+ else if ( action . intent === "reset" ) onReset ?.( ) ;
198+ else if ( action . intent === "link" && action . href ) {
199+ window . open ( action . href , "_blank" , "noopener,noreferrer" ) ;
200+ }
201+ } ;
202+
203+ const actionDisabled = ( action : XAAErrorAction ) => {
204+ if ( action . intent === "configure" ) return ! onConfigure ;
205+ if ( action . intent === "bootstrap" ) return ! onShowBootstrap ;
206+ if ( action . intent === "reset" ) return ! onReset ;
207+ if ( action . intent === "link" ) return ! action . href ;
208+ return true ;
209+ } ;
210+
211+ return (
212+ < div className = { cn ( "rounded-md border px-3 py-2.5 space-y-2" , toneClass ) } >
213+ < div className = "flex items-start gap-2" >
214+ < AlertCircle className = { cn ( "h-4 w-4 mt-0.5 shrink-0" , iconClass ) } />
215+ < div className = "flex-1 min-w-0 space-y-1" >
216+ < div className = "text-xs font-semibold text-foreground" >
217+ { guidance . title }
218+ </ div >
219+ < div className = "text-xs text-muted-foreground" >
220+ { guidance . explanation }
221+ </ div >
222+ </ div >
223+ </ div >
224+ { guidance . actions . length > 0 && (
225+ < div className = "flex flex-wrap gap-2 pl-6" >
226+ { guidance . actions . map ( ( action ) => (
227+ < Button
228+ key = { `${ action . intent } -${ action . label } ` }
229+ type = "button"
230+ variant = "outline"
231+ size = "sm"
232+ className = "h-7 text-xs"
233+ onClick = { ( ) => handleAction ( action ) }
234+ disabled = { actionDisabled ( action ) }
235+ >
236+ { action . label }
237+ </ Button >
238+ ) ) }
239+ </ div >
240+ ) }
241+ </ div >
242+ ) ;
243+ }
244+
245+
66246export function XAAFlowLogger ( {
67247 flowState,
68248 hasProfile,
@@ -264,15 +444,46 @@ export function XAAFlowLogger({
264444 </ div >
265445
266446 < div className = "flex-1 overflow-auto bg-muted/30 p-4 space-y-4" >
267- { flowState . error && (
268- < Alert variant = "destructive" className = "py-2" >
269- < AlertCircle className = "h-4 w-4" />
270- < AlertDescription className = "text-xs" >
271- { flowState . error }
272- </ AlertDescription >
273- </ Alert >
447+ { hasProfile && flowState . compatibilityReport && (
448+ < CompatibilityBanner report = { flowState . compatibilityReport } />
274449 ) }
275450
451+ { ( ( ) => {
452+ const currentStepHttpEntries = ( flowState . httpHistory || [ ] ) . filter (
453+ ( entry ) => entry . step === flowState . currentStep ,
454+ ) ;
455+ const currentStepErroredEntry = latestErroredHttpEntry (
456+ currentStepHttpEntries ,
457+ ) ;
458+ if ( ! flowState . error && ! currentStepErroredEntry ) return null ;
459+ const guidance = getXAAErrorGuidance ( {
460+ step : flowState . currentStep ,
461+ stateError : flowState . error ,
462+ httpEntry : currentStepErroredEntry ,
463+ } ) ;
464+ if ( guidance ) {
465+ return (
466+ < GuidanceCallout
467+ guidance = { guidance }
468+ onConfigure = { actions . onConfigure }
469+ onShowBootstrap = { actions . onShowBootstrap }
470+ onReset = { actions . onReset }
471+ />
472+ ) ;
473+ }
474+ if ( flowState . error ) {
475+ return (
476+ < Alert variant = "destructive" className = "py-2" >
477+ < AlertCircle className = "h-4 w-4" />
478+ < AlertDescription className = "text-xs" >
479+ { flowState . error }
480+ </ AlertDescription >
481+ </ Alert >
482+ ) ;
483+ }
484+ return null ;
485+ } ) ( ) }
486+
276487 { flowState . idJag && flowState . idJagDecoded && (
277488 < IdJagInspector
278489 rawJwt = { flowState . idJag }
@@ -377,6 +588,30 @@ export function XAAFlowLogger({
377588 </ div >
378589 ) : null }
379590
591+ { ( ( ) => {
592+ if ( group . step === flowState . currentStep ) {
593+ // Top-level callout covers the current step.
594+ return null ;
595+ }
596+ const erroredEntry = latestErroredHttpEntry (
597+ group . httpEntries ,
598+ ) ;
599+ if ( ! erroredEntry ) return null ;
600+ const guidance = getXAAErrorGuidance ( {
601+ step : group . step ,
602+ httpEntry : erroredEntry ,
603+ } ) ;
604+ if ( ! guidance ) return null ;
605+ return (
606+ < GuidanceCallout
607+ guidance = { guidance }
608+ onConfigure = { actions . onConfigure }
609+ onShowBootstrap = { actions . onShowBootstrap }
610+ onReset = { actions . onReset }
611+ />
612+ ) ;
613+ } ) ( ) }
614+
380615 { group . infoEntries . map ( ( entry ) => (
381616 < InfoLogEntry
382617 key = { entry . id }
0 commit comments