Skip to content

Commit ea7fd0e

Browse files
better
1 parent 7b6571d commit ea7fd0e

File tree

3 files changed

+127
-114
lines changed

3 files changed

+127
-114
lines changed

mcpjam-inspector/client/src/components/evals/commit-detail-view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,10 @@ export function CommitDetailView({
207207

208208
// Auto-request triage when failures exist and no result yet
209209
useEffect(() => {
210-
if (failedRunIds.length > 0 && !aiTriage.summary && !aiTriage.loading) {
210+
if (failedRunIds.length > 0 && !aiTriage.summary && !aiTriage.loading && !aiTriage.unavailable) {
211211
aiTriage.requestTriage();
212212
}
213-
}, [failedRunIds.length, aiTriage.summary, aiTriage.loading, aiTriage.requestTriage]);
213+
}, [failedRunIds.length, aiTriage.summary, aiTriage.loading, aiTriage.unavailable, aiTriage.requestTriage]);
214214

215215
// Triage summary — shows when any cases failed
216216
const triageSummary = useMemo(() => {
@@ -372,7 +372,7 @@ export function CommitDetailView({
372372
</div>
373373

374374
{/* === AI TRIAGE PANEL === */}
375-
{triageSummary && (aiTriage.summary || aiTriage.loading || aiTriage.error) && (
375+
{triageSummary && !aiTriage.unavailable && (aiTriage.summary || aiTriage.loading || aiTriage.error) && (
376376
<div className="relative rounded-lg border border-orange-200/60 bg-orange-50/30 p-6 shadow-sm dark:border-orange-900/40 dark:bg-orange-950/10">
377377
<div className="absolute top-0 left-0 right-0 h-[3px] rounded-t-lg ai-shimmer-bar" />
378378
<div className="flex items-center gap-2.5 mb-4">

mcpjam-inspector/client/src/components/evals/overview-panel.tsx

Lines changed: 93 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,12 @@ export function OverviewPanel({
276276

277277
const aiOverviewTriage = useCommitTriage(failedOverviewRunIds);
278278

279-
// Auto-request triage when failures exist
279+
// Auto-request triage when failures exist (skip if already unavailable)
280280
useEffect(() => {
281-
if (failedOverviewRunIds.length > 0 && !aiOverviewTriage.summary && !aiOverviewTriage.loading) {
281+
if (failedOverviewRunIds.length > 0 && !aiOverviewTriage.summary && !aiOverviewTriage.loading && !aiOverviewTriage.unavailable) {
282282
aiOverviewTriage.requestTriage();
283283
}
284-
}, [failedOverviewRunIds.length, aiOverviewTriage.summary, aiOverviewTriage.loading, aiOverviewTriage.requestTriage]);
284+
}, [failedOverviewRunIds.length, aiOverviewTriage.summary, aiOverviewTriage.loading, aiOverviewTriage.unavailable, aiOverviewTriage.requestTriage]);
285285

286286
// Pre-compute inline failure tags for the failure feed
287287
// Tags suites with failed cases OR failed result
@@ -490,7 +490,7 @@ export function OverviewPanel({
490490
</div>
491491

492492
{/* AI Overview Summary — only when failures exist and triage is active */}
493-
{failedOverviewRunIds.length > 0 && (aiOverviewTriage.summary || aiOverviewTriage.loading || aiOverviewTriage.error) && (
493+
{failedOverviewRunIds.length > 0 && !aiOverviewTriage.unavailable && (aiOverviewTriage.summary || aiOverviewTriage.loading || aiOverviewTriage.error) && (
494494
<div className="relative rounded-lg border border-orange-200/60 bg-orange-50/30 shadow-sm dark:border-orange-900/40 dark:bg-orange-950/10">
495495
<div className="absolute top-0 left-0 right-0 h-[3px] rounded-t-lg ai-shimmer-bar" />
496496
<div className="px-4 py-3">
@@ -626,108 +626,102 @@ export function OverviewPanel({
626626
</div>
627627
)}
628628

629-
{/* Section C: Failure Feed (Needs Attention) */}
630-
<Collapsible open={hasFailures && failureFeedOpen} onOpenChange={setFailureFeedOpen}>
631-
<div className="rounded-xl border bg-card">
632-
<CollapsibleTrigger className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/50 transition-colors rounded-xl">
633-
<div className="flex items-center gap-2">
634-
{failureFeedOpen && hasFailures ? (
635-
<ChevronDown className="h-4 w-4 text-muted-foreground" />
636-
) : (
637-
<ChevronRight className="h-4 w-4 text-muted-foreground" />
638-
)}
639-
<span className="text-sm font-semibold">Needs Attention</span>
640-
{hasFailures && (
629+
{/* Section C: Failure Feed (Needs Attention) — hidden when nothing needs attention */}
630+
{hasFailures && (
631+
<Collapsible open={failureFeedOpen} onOpenChange={setFailureFeedOpen}>
632+
<div className="rounded-xl border bg-card">
633+
<CollapsibleTrigger className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/50 transition-colors rounded-xl">
634+
<div className="flex items-center gap-2">
635+
{failureFeedOpen ? (
636+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
637+
) : (
638+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
639+
)}
640+
<span className="text-sm font-semibold">Needs Attention</span>
641641
<span className="text-xs text-muted-foreground">
642642
({failureEntries.length})
643643
</span>
644-
)}
645-
</div>
646-
{!hasFailures && (
647-
<div className="flex items-center gap-1.5 text-xs text-emerald-500">
648-
<CheckCircle2 className="h-3.5 w-3.5" />
649-
All clear
650644
</div>
651-
)}
652-
</CollapsibleTrigger>
653-
654-
<CollapsibleContent>
655-
<div className="border-t divide-y">
656-
{failureEntries.map((entry) => {
657-
const isFailed = entry.latestRun?.result === "failed";
658-
const isNeverRun = !entry.latestRun;
659-
660-
return (
661-
<button
662-
key={entry.suite._id}
663-
onClick={() => onSelectSuite?.(entry.suite._id)}
664-
className="w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors"
665-
>
666-
<div className="flex items-start gap-2.5">
667-
{isFailed ? (
668-
<XCircle className="h-4 w-4 text-destructive shrink-0 mt-0.5" />
669-
) : (
670-
<MinusCircle className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
671-
)}
672-
<div className="min-w-0 flex-1">
673-
<div className="flex items-center gap-1.5">
674-
<span className="text-sm font-medium truncate">
675-
{entry.suite.name}
676-
</span>
677-
{isFailed &&
678-
failureTagMap.get(entry.suite._id)?.map((tag) => (
679-
<InlineFailureTag key={tag} tag={tag} />
680-
))}
681-
</div>
682-
{isFailed && entry.latestRun?.summary && (
683-
<div className="text-xs text-muted-foreground mt-0.5">
684-
{entry.latestRun.summary.passed}/
685-
{entry.latestRun.summary.total} tests passed
686-
{entry.latestRun.summary.passRate !== undefined &&
687-
` (${Math.round(entry.latestRun.summary.passRate)}%)`}
688-
</div>
689-
)}
690-
{isFailed && entry.latestRun?.ciMetadata && (
691-
<div className="text-[11px] text-muted-foreground mt-0.5">
692-
{entry.latestRun.ciMetadata.branch && (
693-
<span>{entry.latestRun.ciMetadata.branch}</span>
694-
)}
695-
{entry.latestRun.ciMetadata.commitSha && (
696-
<span>
697-
{" "}
698-
@ {entry.latestRun.ciMetadata.commitSha.slice(0, 7)}
699-
</span>
700-
)}
701-
{" · "}
702-
{formatRelativeTime(
703-
entry.latestRun.completedAt ??
704-
entry.latestRun.createdAt,
705-
)}
706-
</div>
707-
)}
708-
{isFailed && !entry.latestRun?.ciMetadata && (
709-
<div className="text-[11px] text-muted-foreground mt-0.5">
710-
{formatRelativeTime(
711-
entry.latestRun?.completedAt ??
712-
entry.latestRun?.createdAt,
713-
)}
714-
</div>
645+
</CollapsibleTrigger>
646+
647+
<CollapsibleContent>
648+
<div className="border-t divide-y">
649+
{failureEntries.map((entry) => {
650+
const isFailed = entry.latestRun?.result === "failed";
651+
const isNeverRun = !entry.latestRun;
652+
653+
return (
654+
<button
655+
key={entry.suite._id}
656+
onClick={() => onSelectSuite?.(entry.suite._id)}
657+
className="w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors"
658+
>
659+
<div className="flex items-start gap-2.5">
660+
{isFailed ? (
661+
<XCircle className="h-4 w-4 text-destructive shrink-0 mt-0.5" />
662+
) : (
663+
<MinusCircle className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
715664
)}
716-
{isNeverRun && (
717-
<div className="text-xs text-muted-foreground mt-0.5">
718-
Never run
665+
<div className="min-w-0 flex-1">
666+
<div className="flex items-center gap-1.5">
667+
<span className="text-sm font-medium truncate">
668+
{entry.suite.name}
669+
</span>
670+
{isFailed &&
671+
failureTagMap.get(entry.suite._id)?.map((tag) => (
672+
<InlineFailureTag key={tag} tag={tag} />
673+
))}
719674
</div>
720-
)}
675+
{isFailed && entry.latestRun?.summary && (
676+
<div className="text-xs text-muted-foreground mt-0.5">
677+
{entry.latestRun.summary.passed}/
678+
{entry.latestRun.summary.total} tests passed
679+
{entry.latestRun.summary.passRate !== undefined &&
680+
` (${Math.round(entry.latestRun.summary.passRate)}%)`}
681+
</div>
682+
)}
683+
{isFailed && entry.latestRun?.ciMetadata && (
684+
<div className="text-[11px] text-muted-foreground mt-0.5">
685+
{entry.latestRun.ciMetadata.branch && (
686+
<span>{entry.latestRun.ciMetadata.branch}</span>
687+
)}
688+
{entry.latestRun.ciMetadata.commitSha && (
689+
<span>
690+
{" "}
691+
@ {entry.latestRun.ciMetadata.commitSha.slice(0, 7)}
692+
</span>
693+
)}
694+
{" · "}
695+
{formatRelativeTime(
696+
entry.latestRun.completedAt ??
697+
entry.latestRun.createdAt,
698+
)}
699+
</div>
700+
)}
701+
{isFailed && !entry.latestRun?.ciMetadata && (
702+
<div className="text-[11px] text-muted-foreground mt-0.5">
703+
{formatRelativeTime(
704+
entry.latestRun?.completedAt ??
705+
entry.latestRun?.createdAt,
706+
)}
707+
</div>
708+
)}
709+
{isNeverRun && (
710+
<div className="text-xs text-muted-foreground mt-0.5">
711+
Never run
712+
</div>
713+
)}
714+
</div>
715+
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
721716
</div>
722-
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
723-
</div>
724-
</button>
725-
);
726-
})}
727-
</div>
728-
</CollapsibleContent>
729-
</div>
730-
</Collapsible>
717+
</button>
718+
);
719+
})}
720+
</div>
721+
</CollapsibleContent>
722+
</div>
723+
</Collapsible>
724+
)}
731725

732726
{/* Section D: Suite Table */}
733727
<div className="rounded-xl border bg-card">

mcpjam-inspector/client/src/components/evals/use-ai-triage.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useMemo, useEffect } from "react";
1+
import { useState, useCallback, useEffect, useRef } from "react";
22
import { useMutation } from "convex/react";
33

44
// ---------------------------------------------------------------------------
@@ -25,38 +25,57 @@ export interface TriageResult {
2525
* The backend generates the summary asynchronously (action → AI SDK → save).
2626
*
2727
* Until the backend mutation is deployed, requests fail gracefully and the
28-
* panel shows "AI triage unavailable" instead of crashing.
28+
* panel stays hidden instead of flashing briefly.
2929
*/
3030
export function useCommitTriage(
3131
failedRunIds: string[],
3232
): {
3333
summary: string | null;
3434
loading: boolean;
3535
error: string | null;
36+
unavailable: boolean;
3637
requestTriage: () => void;
3738
} {
3839
const [loading, setLoading] = useState(false);
3940
const [error, setError] = useState<string | null>(null);
4041
const [summary, setSummary] = useState<string | null>(null);
4142
const [unavailable, setUnavailable] = useState(false);
4243

43-
// Reset state when the run IDs change (navigating to a different commit)
44-
const runKey = failedRunIds.join(",");
45-
useEffect(() => {
46-
setSummary(null);
47-
setError(null);
48-
setLoading(false);
49-
setUnavailable(false);
50-
}, [runKey]);
51-
44+
// Track whether the mutation exists at the module level (survives re-renders)
5245
let requestTriageMutation: ReturnType<typeof useMutation> | null = null;
46+
let mutationExists = true;
5347
try {
5448
// eslint-disable-next-line react-hooks/rules-of-hooks
5549
requestTriageMutation = useMutation("testSuites:requestTriage" as any);
5650
} catch {
5751
// Mutation not registered yet — backend not deployed
52+
mutationExists = false;
5853
}
5954

55+
// Once we know the mutation doesn't exist, mark unavailable permanently
56+
// (no state update needed on subsequent renders since unavailable is already true)
57+
const mutationExistsRef = useRef(mutationExists);
58+
mutationExistsRef.current = mutationExists;
59+
60+
useEffect(() => {
61+
if (!mutationExistsRef.current) {
62+
setUnavailable(true);
63+
}
64+
}, []);
65+
66+
// Reset state when the run IDs change (navigating to a different commit),
67+
// but preserve unavailable if the mutation doesn't exist
68+
const runKey = failedRunIds.join(",");
69+
useEffect(() => {
70+
setSummary(null);
71+
setError(null);
72+
setLoading(false);
73+
// Only reset unavailable if the mutation actually exists
74+
if (mutationExistsRef.current) {
75+
setUnavailable(false);
76+
}
77+
}, [runKey]);
78+
6079
const requestTriage = useCallback(() => {
6180
if (failedRunIds.length === 0 || unavailable) return;
6281
if (!requestTriageMutation) {
@@ -94,5 +113,5 @@ export function useCommitTriage(
94113
});
95114
}, [failedRunIds, requestTriageMutation, unavailable]);
96115

97-
return { summary, loading, error, requestTriage };
116+
return { summary, loading, error, unavailable, requestTriage };
98117
}

0 commit comments

Comments
 (0)