@@ -178,32 +178,42 @@ const VERDICT_META: Record<
178178 unclear : { label : "Unclear" , text : "text-warn" , dot : "bg-warn" } ,
179179} ;
180180
181- /** The AI investigator's call on the paused gate: a compact verdict chip (with a
182- sparkle so it reads as the AI's take, not a status), and the full reasoning on
183- hover, so the human sees what the agent concluded right where they decide . */
184- const RecommendationChip = ( {
181+ /** The AI investigator's call, a small spark button beside Approve; on hover it shows
182+ the verdict + the full reasoning. Tinted by the verdict so a glance already hints
183+ the direction, the detail is one hover away, no trace drawer needed . */
184+ const RecommendationSpark = ( {
185185 recommendation,
186+ stepId,
186187} : {
187188 recommendation : NonNullable < NodeData [ "recommendation" ] > ;
189+ stepId : string ;
188190} ) => {
189191 const meta = VERDICT_META [ recommendation . verdict ] ;
190- const chip = (
191- < span
192- className = { `inline-flex w-full items-center gap-1.5 rounded-lg bg-subtle/60 px-2 py-1 text-[11px] font-medium ${ meta . text } ` }
193- >
194- < span aria-hidden > ✦</ span >
195- < span className = "text-faint" > AI:</ span >
196- < span className = { `inline-block size-1.5 rounded-full ${ meta . dot } ` } />
197- { meta . label }
198- </ span >
199- ) ;
200- if ( ! recommendation . rationale ) return chip ;
201192 return (
202193 < TooltipProvider >
203194 < Tooltip >
204- < TooltipTrigger asChild > { chip } </ TooltipTrigger >
195+ < TooltipTrigger asChild >
196+ < button
197+ type = "button"
198+ data-testid = { `gate-ai-${ stepId } ` }
199+ aria-label = { `AI recommendation: ${ meta . label } ` }
200+ className = { `grid h-7 w-7 shrink-0 place-items-center rounded-lg text-[13px] ring-1 ring-inset ring-line transition-colors hover:bg-subtle ${ meta . text } ` }
201+ >
202+ < span aria-hidden > ✦</ span >
203+ </ button >
204+ </ TooltipTrigger >
205205 < TooltipContent side = "bottom" className = "max-w-[280px]" >
206- { recommendation . rationale }
206+ < span className = "flex items-center gap-1.5 font-medium" >
207+ < span
208+ className = { `inline-block size-1.5 rounded-full ${ meta . dot } ` }
209+ />
210+ AI: { meta . label }
211+ </ span >
212+ { recommendation . rationale && (
213+ < span className = "mt-1 block text-muted" >
214+ { recommendation . rationale }
215+ </ span >
216+ ) }
207217 </ TooltipContent >
208218 </ Tooltip >
209219 </ TooltipProvider >
@@ -387,9 +397,6 @@ const StepNode = ({ data }: NodeProps<Node<NodeData>>) => {
387397 position = { Position . Bottom }
388398 className = "nodrag nopan flex w-[244px] flex-col gap-1.5 rounded-xl bg-surface p-2 shadow-lift ring-1 ring-inset ring-line"
389399 >
390- { recommendation && (
391- < RecommendationChip recommendation = { recommendation } />
392- ) }
393400 < div className = "flex gap-1.5" >
394401 < button
395402 type = "button"
@@ -415,6 +422,15 @@ const StepNode = ({ data }: NodeProps<Node<NodeData>>) => {
415422 >
416423 Approve
417424 </ button >
425+ { /* The AI investigator's take, a spark to the right of Approve; hover
426+ reveals the verdict + reasoning. Quiet by default so it informs the
427+ decision without shouting over the buttons. */ }
428+ { recommendation && (
429+ < RecommendationSpark
430+ recommendation = { recommendation }
431+ stepId = { step . id }
432+ />
433+ ) }
418434 </ div >
419435 { onReason && choice === "reject" && (
420436 < input
@@ -731,8 +747,8 @@ const Inner = ({
731747 focus . length > 0
732748 ? {
733749 nodes : focus . map ( ( id ) => ( { id } ) ) ,
734- padding : 0.6 ,
735- maxZoom : 1 ,
750+ padding : 0.25 ,
751+ maxZoom : 1.1 ,
736752 duration : 200 ,
737753 }
738754 : { padding : 0.18 , duration : 200 } ,
@@ -887,9 +903,18 @@ const Inner = ({
887903 if ( ! initialized || laidOutFor . current !== graphKey || focusKey === "" )
888904 return ;
889905 const ids = focusKey . split ( "|" ) . map ( ( id ) => ( { id } ) ) ;
906+ // Frame the pending gate itself, tightly. A large padding made fitView zoom out
907+ // until the WHOLE graph fit (the gate no longer looked focused); a small padding
908+ // keeps the gate centered and legible. maxZoom caps how far it zooms in on a lone
909+ // node so it doesn't blow up. A rAF lets the just-laid-out measurements settle.
890910 const raf = requestAnimationFrame (
891911 ( ) =>
892- void fitView ( { nodes : ids , duration : 400 , padding : 0.6 , maxZoom : 1 } ) ,
912+ void fitView ( {
913+ nodes : ids ,
914+ duration : 400 ,
915+ padding : 0.25 ,
916+ maxZoom : 1.1 ,
917+ } ) ,
893918 ) ;
894919 return ( ) => cancelAnimationFrame ( raf ) ;
895920 } , [ focusKey , initialized , graphKey , fitView ] ) ;
0 commit comments