Skip to content

Commit 1dadedf

Browse files
ok
1 parent 7c2ec0a commit 1dadedf

File tree

3 files changed

+127
-121
lines changed

3 files changed

+127
-121
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 & 106 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
@@ -491,7 +491,7 @@ export function OverviewPanel({
491491
</div>
492492

493493
{/* AI Overview Summary — only when failures exist and triage is active */}
494-
{failedOverviewRunIds.length > 0 && (aiOverviewTriage.summary || aiOverviewTriage.loading || aiOverviewTriage.error) && (
494+
{failedOverviewRunIds.length > 0 && !aiOverviewTriage.unavailable && (aiOverviewTriage.summary || aiOverviewTriage.loading || aiOverviewTriage.error) && (
495495
<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">
496496
<div className="absolute top-0 left-0 right-0 h-[3px] rounded-t-lg ai-shimmer-bar" />
497497
<div className="px-4 py-3">
@@ -627,115 +627,102 @@ export function OverviewPanel({
627627
</div>
628628
)}
629629

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

740727
{/* Section D: Suite Table */}
741728
<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)