Skip to content

Commit 4f8c228

Browse files
DylanMerigaudclaude
andcommitted
#3 AI reco as a spark button beside Approve; #5 frame the pending gate tightly
#3: replace the full-width "✦ AI: <verdict>" text chip with a small spark button to the right of Approve. Tinted by the verdict, and hover shows the verdict + reasoning. Quieter, it informs the decision without shouting over the buttons. #5: the focus fitView used padding 0.6, which zoomed out until the whole graph fit, so the pending gate never looked focused. Tighten to padding 0.25 + maxZoom 1.1 so the gate you must act on lands centered and legible. NOTE: gate green; not reconfirmed live (INV-2042 pause kept timing out). To verify: pause on INV-2042 and check the spark sits beside Approve (hover = reco) and the view frames the gate, not the whole workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d240772 commit 4f8c228

1 file changed

Lines changed: 48 additions & 23 deletions

File tree

components/workflow-graph.tsx

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)