Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
90922a2
feat: surface approx model size on collapsed recipe group headers
tonythethompson Aug 13, 2026
24fc7e0
fix: block Execute Live and suppress model-tuning advisories with no …
tonythethompson Aug 13, 2026
1b6a4bc
Update Cargo.lock
tonythethompson Aug 13, 2026
b2a4d0b
feat: collapse export/platform hardware targets on step 02 by default
tonythethompson Aug 13, 2026
37c9470
feat: connect Recipe & run output to Playground
tonythethompson Aug 13, 2026
506136d
fix: address PR #260 review feedback
tonythethompson Aug 13, 2026
3cd3065
fix: prevent stale state in Playground and RecipeValidationPanel
tonythethompson Aug 13, 2026
30e2701
fix: reset diagnostic ref when no critical issues found
tonythethompson Aug 13, 2026
ab9ad45
fix: next pass of design fixes for pipeline UI
tonythethompson Aug 13, 2026
9810649
Merge branch 'main' into fix/olive-studio-issue-reports
tonythethompson Aug 13, 2026
48654b9
fix: auto-expand provider card when selected (sync isExpanded with is…
tonythethompson Aug 13, 2026
eacf11b
Capture completed run recipe for Playground
tonythethompson Aug 13, 2026
07d6e23
fix: Preserve local model selection for batch validation
qodo-code-review[bot] Aug 13, 2026
145c08f
fix: preserve Execute run context in Playground and improve UX/a11y
tonythethompson Aug 13, 2026
46d096e
Fix stale execute recipe and diagnostic state
tonythethompson Aug 13, 2026
12c64ae
fix: unblock CPU fallback on null probe & filter WebGPU recipes for B…
tonythethompson Aug 13, 2026
677aaf8
Fix provider details aria-controls IDs
tonythethompson Aug 13, 2026
7b7e8d2
perf: memoize captured WebGPU recipe check and update comments
tonythethompson Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InputEnvironmentPanel } from "@/components/features/input/InputEnvironm
import { LicenseNotice } from "@/components/LicenseNotice";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { usePipelineState } from "@/lib/stores/pipelineStore";
import { hasSelectedModel } from "@/lib/pipelineValidation";
import type { ReportArea } from "@/lib/issueReport";
import { VramEstimateBanner } from "@/components/features/VramEstimateBanner";
import { KbSyncIndicator } from "@/components/features/KbSyncIndicator";
Expand Down Expand Up @@ -236,12 +237,14 @@ function Dashboard() {
<div className="space-y-0.5">
{SECTIONS.map(({ id, step, label, icon: Icon }) => {
const isActive = activeView === id;
const modelSelected = hasSelectedModel(pipelineState);
const isIncomplete = !modelSelected && id !== "input";
return (
<button
key={id}
type="button"
title={`${step} ${label}`}
aria-label={`${step} ${label}`}
title={isIncomplete ? `${step} ${label} — select a model first` : `${step} ${label}`}
aria-label={isIncomplete ? `${step} ${label}, incomplete` : `${step} ${label}`}
onClick={() => scrollToSection(id)}
aria-current={isActive ? "step" : undefined}
disabled={isOliveRunning && id !== "execute" && id !== "playground"}
Expand All @@ -258,11 +261,17 @@ function Dashboard() {
>
<span
className={cn(
"text-[11px] tabular-nums shrink-0 w-5 text-center wide:text-left",
"relative text-[11px] tabular-nums shrink-0 w-5 text-center wide:text-left",
isActive ? "text-electric-blue" : "text-slate-400",
)}
>
{step}
{isIncomplete && (
<span
className="absolute -top-0.5 -right-0.5 wide:right-auto wide:left-[calc(100%-2px)] h-1.5 w-1.5 rounded-full bg-amber-500"
aria-hidden
/>
)}
</span>
<Icon
className={cn(
Expand Down
1 change: 1 addition & 0 deletions src/components/features/execute/BatchProcessingPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ function uiStateForBatchJob(job: BatchJob, state: UIState): UIState {
return {
...state,
modelSource: job.modelSource,
localFiles: state.localFiles,
hfModelId: job.modelSource === "huggingface" ? job.modelIdentifier : state.hfModelId,
azureModelPath: job.modelSource === "azure" ? job.modelIdentifier : state.azureModelPath,
ihvProvider: job.provider,
Expand Down
11 changes: 11 additions & 0 deletions src/components/features/execute/ExecutionWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,10 @@
executionStatus,
executionExitCode,
gpuMetrics,
runRecipeJson,

Check notice on line 480 in src/components/features/execute/ExecutionWorkspace.tsx

View check run for this annotation

codefactor.io / CodeFactor

src/components/features/execute/ExecutionWorkspace.tsx#L480

'runRecipeJson' is assigned a value but never used. Allowed unused vars must match /^_/u. (@typescript-eslint/no-unused-vars)

Check failure on line 480 in src/components/features/execute/ExecutionWorkspace.tsx

View workflow job for this annotation

GitHub Actions / validate

Property 'runRecipeJson' does not exist on type 'UseOliveStreamReturn'.
handleExecuteLive,
handleCancelJob,
runRecipeJsonRef,
} = useOliveStream({
state,
hardwareProbe,
Expand Down Expand Up @@ -577,6 +579,15 @@
}
}, [executionStatus, executionLogs, fetchKeyedDiagnostic]);

// Capture the submitted recipe when Execute completes so Playground uses the exact recipe that ran,
// not a rebuilt version that might include post-run state edits.
const setCapturedRunRecipe = usePlaygroundStore((s) => s.setCapturedRunRecipe);
useEffect(() => {
if (executionStatus === "completed" && runRecipeJsonRef.current) {
setCapturedRunRecipe(runRecipeJsonRef.current);
}
}, [executionStatus, setCapturedRunRecipe, runRecipeJsonRef]);

// Auto-save completed diagnoses to history
const prevDiagnosticRef = useRef(mcpDiagnostic);
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function RecipeValidationPanel({ state, setState }: RecipeValidationPanel
const [showCompatDetails, setShowCompatDetails] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const forceRefreshRef = useRef(false);
const { diagnostic: mcpDiagnostic, isDiagnosing: mcpDiagnosing, fetchDiagnostic } = useMcpDiagnostic();
const { diagnostic: mcpDiagnostic, isDiagnosing: mcpDiagnosing, fetchDiagnostic, clearDiagnostic } = useMcpDiagnostic();

const validation = getPipelineValidation(state);
// Pass-parameter advisories (quant method preferences, precision tips, etc.) are all
Expand Down Expand Up @@ -219,16 +219,21 @@ export function RecipeValidationPanel({ state, setState }: RecipeValidationPanel
// Reset ref and clear stale diagnostics when model is deselected.
if (!modelSelected) {
prevIssueCountRef.current = 0;
clearDiagnostic();
return;
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
const criticalIssues = validation.issues.filter((i) => i.severity === "critical");
const count = criticalIssues.length;
if (count === 0 || count === prevIssueCountRef.current) return;
if (count === 0) {
prevIssueCountRef.current = 0;
return;
}
if (count === prevIssueCountRef.current) return;
prevIssueCountRef.current = count;

const logLines = criticalIssues.map((i) => `[VALIDATION] ${i.title}: ${i.description}`);
fetchDiagnostic(logLines);
}, [validation.issues, modelSelected, fetchDiagnostic]);
}, [validation.issues, modelSelected, fetchDiagnostic, clearDiagnostic]);

// Hardware-specific parameter validation (synchronous, cheap — runs every render with state)
const paramWarnings = validatePassParameters(state, activePassNames, recipe);
Expand Down
2 changes: 2 additions & 0 deletions src/components/features/execute/useOliveStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
gpuMetrics: GpuMetrics | null;
handleExecuteLive: () => Promise<void>;
handleCancelJob: () => Promise<void>;
runRecipeJsonRef: React.MutableRefObject<string | null>;
}

type TerminalStatus = "completed" | "failed" | "cancelled";
Expand Down Expand Up @@ -133,50 +134,50 @@
);

const finalizeExecution = useCallback(
(
generation: number,
args: {
jobId: string | null;
status: TerminalStatus;
exitCode?: number | null;
/** When false, trust the provided status without log/exit heuristics. */
applyOutcomeHeuristics?: boolean;
},
) => {
if (!isCurrentRun(generation)) return;

let finalStatus = args.status;
const exitCode = args.exitCode ?? null;
const applyHeuristics = args.applyOutcomeHeuristics !== false;

if (applyHeuristics && finalStatus !== "cancelled") {
if (exitCode === null) {
finalStatus = "failed";
} else {
const failed = exitCode !== 0 || logsIndicateFailure(executionLogsRef.current);
finalStatus = failed ? "failed" : "completed";
}
}

const reportedExit =
finalStatus === "cancelled"
? exitCode !== null && exitCode !== 0
? exitCode
: null
: exitCode === null || (finalStatus === "failed" && exitCode === 0)
? 1
: exitCode;

setExecutionStatus(finalStatus);
setExecutionExitCode(reportedExit);
setIsRunning(false);
setGpuMetrics(null);
onRunStateChange?.(false);
if (args.jobId) {
recordJobCompletion(args.jobId, finalStatus, reportedExit);
}
if (finalStatus === "failed") setMcpFixApplied("");
},

Check notice on line 180 in src/components/features/execute/useOliveStream.ts

View check run for this annotation

codefactor.io / CodeFactor

src/components/features/execute/useOliveStream.ts#L137-L180

Complex Method
[isCurrentRun, onRunStateChange, recordJobCompletion, setMcpFixApplied],
);

Expand Down Expand Up @@ -221,287 +222,287 @@
}
}, [liveJobId, setExecutionLogs, beginNewRunEpoch, finalizeExecution]);

const handleExecuteLive = useCallback(async () => {
if (isRunning) return;

const fresh = buildRecipeFromState(state, { hardwareProbe });

if (!fresh.isRunnable) {
const blockingCount = fresh.validation.criticalCount + fresh.localExecutionIssues.length;
const freshSchemaErrors = fresh.schema.errors ?? [];
const freshBlockLines = fresh.localExecutionIssues.map(
(issue) => `[BLOCK] ${issue.title}: ${issue.description}`,
);
const generation = beginNewRunEpoch();
setExecutionLogs([
fresh.schema.valid
? `[ERROR] Cannot execute: ${blockingCount} blocking issue(s).`
: `[ERROR] Cannot execute: recipe schema invalid.`,
...(fresh.schema.valid
? [
...fresh.validation.issues
.filter((issue) => issue.severity === "critical")
.map((issue) => `[BLOCK] ${issue.title}: ${issue.description}`),
...freshBlockLines,
]
: freshSchemaErrors.map((e) => `[SCHEMA] ${e}`)),
]);
finalizeExecution(generation, {
jobId: null,
status: "failed",
exitCode: null,
applyOutcomeHeuristics: false,
});
return;
}

const generation = beginNewRunEpoch();
const runAbort = new AbortController();
runAbortRef.current = runAbort;

runRecipeJsonRef.current = fresh.recipeJson;
pendingCancelRef.current = false;
setLiveJobId(null);
setState({ activeJobId: null });
setIsRunning(true);
onRunStateChange?.(true);
setExecutionLogs(["[INFO] Initiating Olive run...\n"]);
setExecutionStatus("running");
setExecutionExitCode(null);
setGpuMetrics(null);
runStartTimeRef.current = Date.now();

try {
const resp = await fetch("/api/olive/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ recipeJson: fresh.recipeJson, cudaVersion: state.cudaVersion ?? "auto" }),
signal: runAbort.signal,
});

if (!isCurrentRun(generation)) return;

if (!resp.ok) {
const errData = await resp.json().catch(() => ({ error: `HTTP ${resp.status}` }));
if (!isCurrentRun(generation)) return;
setExecutionLogs((prev) => [...prev, `[ERROR] ${errData.error}`]);
pendingCancelRef.current = false;
finalizeExecution(generation, {
jobId: null,
status: "failed",
exitCode: null,
applyOutcomeHeuristics: false,
});
return;
}

const { jobId } = (await resp.json()) as { jobId: string };
if (!isCurrentRun(generation)) return;

setLiveJobId(jobId);
setState({ activeJobId: jobId });

if (pendingCancelRef.current) {
pendingCancelRef.current = false;
setExecutionLogs((prev) => [
...prev,
"[INFO] Applying queued cancel now that the job id is assigned...",
]);
try {
const cancelResp = await fetch("/api/olive/cancel", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jobId }),
signal: runAbort.signal,
});
if (!isCurrentRun(generation)) return;
const cancelData = (await cancelResp.json().catch(() => ({}))) as {
status?: string;
error?: string;
};
if (!isCurrentRun(generation)) return;
if (cancelResp.ok && cancelData.status === "cancelled") {
setExecutionLogs((prev) => [...prev, "[INFO] Cancellation signal confirmed by server."]);
finalizeExecution(generation, {
jobId,
status: "cancelled",
exitCode: null,
applyOutcomeHeuristics: false,
});
return;
}
setExecutionLogs((prev) => [
...prev,
cancelResp.ok
? `[INFO] Queued cancel found job already ${cancelData.status ?? "finished"}. Connecting stream.`
: `[ERROR] Queued cancel failed: ${cancelData.error ?? "unknown"}. Continuing with stream.`,
]);
} catch (err: unknown) {
if (!isCurrentRun(generation)) return;
if (err instanceof DOMException && err.name === "AbortError") return;
const message = err instanceof Error ? err.message : String(err);
setExecutionLogs((prev) => [
...prev,
`[ERROR] Queued cancel failed: ${message}. Continuing with stream.`,
]);
}
}

if (!isCurrentRun(generation)) return;

let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;
const MAX_BACKOFF_MS = 30000;
const connectSSE = (targetJobId: string) => {
if (!isCurrentRun(generation)) return;
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
liveSourceRef.current?.close();

const evtSource = new EventSource(`/api/olive/stream/${targetJobId}`);
liveSourceRef.current = evtSource;

evtSource.onopen = () => {
if (!isCurrentRun(generation)) return;
if (reconnectAttempts > 0) {
setExecutionLogs((prev) => [...prev, "[INFO] Stream reconnected successfully."]);
}
reconnectAttempts = 0;
};

evtSource.addEventListener("log", (e: MessageEvent) => {
if (!isCurrentRun(generation)) return;
try {
const payload = JSON.parse(String(e.data)) as { line?: unknown };
if (typeof payload.line === "string" && payload.line.length > 0) {
const line = payload.line;
setExecutionLogs((prev) => [...prev, line]);
}
} catch {
/* ignore malformed */
}
});

evtSource.addEventListener("metrics", (e: MessageEvent) => {
if (!isCurrentRun(generation)) return;
try {
const parsed: unknown = JSON.parse(e.data);
const metrics = parseGpuMetrics(parsed);
if (metrics) setGpuMetrics(metrics);
} catch {
/* ignore malformed */
}
});

evtSource.addEventListener("done", (e: MessageEvent) => {
if (!isCurrentRun(generation)) return;
let exitCode: number | null = null;
let serverStatus: string | undefined;
try {
const payload = JSON.parse(e.data) as { exitCode?: number | null; status?: string };
exitCode = typeof payload.exitCode === "number" ? payload.exitCode : null;
serverStatus = payload.status;
} catch {
exitCode = null;
}
const status: TerminalStatus =
serverStatus === "cancelled" ? "cancelled" : exitCode === null ? "failed" : "completed";
finalizeExecution(generation, {
jobId: targetJobId,
status,
exitCode,
applyOutcomeHeuristics: status !== "cancelled",
});
evtSource.close();
if (liveSourceRef.current === evtSource) {
liveSourceRef.current = null;
}
});

evtSource.onerror = async () => {
evtSource.close();
if (liveSourceRef.current === evtSource) {
liveSourceRef.current = null;
}
if (!isCurrentRun(generation)) return;

let serverSaysRunning = false;
try {
statusAbortRef.current?.abort();
const statusAbort = new AbortController();
statusAbortRef.current = statusAbort;
const statusResp = await fetch(`/api/olive/status/${targetJobId}`, {
signal: statusAbort.signal,
});
if (!isCurrentRun(generation)) return;
if (statusResp.ok) {
const statusData = await statusResp.json();
if (!isCurrentRun(generation)) return;
if (
statusData.status === "completed" ||
statusData.status === "failed" ||
statusData.status === "cancelled"
) {
const finalStatus: TerminalStatus =
statusData.status === "completed"
? "completed"
: statusData.status === "cancelled"
? "cancelled"
: "failed";
finalizeExecution(generation, {
jobId: targetJobId,
status: finalStatus,
exitCode:
typeof statusData.exitCode === "number"
? statusData.exitCode
: finalStatus === "completed"
? 0
: 1,
applyOutcomeHeuristics: finalStatus !== "cancelled",
});
return;
} else if (statusData.status === "running" || statusData.status === "setting_up") {
serverSaysRunning = true;
}
}
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") return;
/* ignore status check failure */
}

if (!isCurrentRun(generation)) return;

if (serverSaysRunning || reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
const backoffMs = Math.min(1000 * Math.pow(1.5, reconnectAttempts), MAX_BACKOFF_MS);
setExecutionLogs((prev) => [
...prev,
`[WARN] Stream connection lost. Reconnecting (attempt ${reconnectAttempts}${serverSaysRunning ? "" : `/${MAX_RECONNECT_ATTEMPTS}`} in ${(backoffMs / 1000).toFixed(1)}s)...`,
]);

if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
reconnectTimeoutRef.current = setTimeout(() => {
reconnectTimeoutRef.current = null;
if (isCurrentRun(generation)) connectSSE(targetJobId);
}, backoffMs);
} else {
setExecutionLogs((prev) => [
...prev,
"[ERROR] SSE connection lost permanently after maximum retry attempts.",
]);
finalizeExecution(generation, {
jobId: targetJobId,
status: "failed",
exitCode: 1,
applyOutcomeHeuristics: false,
});
}
};

Check notice on line 505 in src/components/features/execute/useOliveStream.ts

View check run for this annotation

codefactor.io / CodeFactor

src/components/features/execute/useOliveStream.ts#L424-L505

Complex Method
};

connectSSE(jobId);
Expand Down Expand Up @@ -540,5 +541,6 @@
gpuMetrics,
handleExecuteLive,
handleCancelJob,
runRecipeJsonRef,
};
}
6 changes: 3 additions & 3 deletions src/components/features/ihv/HardwareCompatibilityMatrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ export function HardwareCompatibilityMatrix({
<TooltipProvider delayDuration={150}>
<div className="overflow-hidden rounded-xl border border-slate-800/80 bg-slate-950/25 mt-2 shadow-xl animate-in fade-in duration-300">
<div
className="overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-electric-blue focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
className="max-h-[560px] overflow-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-electric-blue focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
tabIndex={0}
role="region"
aria-label="Pass and execution provider compatibility matrix"
>
<table className="w-full text-left border-collapse min-w-[720px]">
<thead>
<tr className="border-b border-slate-800/80 bg-slate-900/30">
<thead className="sticky top-0 z-10">
<tr className="border-b border-slate-800/80 bg-slate-900">
{/* Header Cell 1 */}
<th className="p-2 px-3 text-[11px] font-mono font-semibold tracking-wider text-slate-400 w-[200px]">
PASS
Expand Down
174 changes: 102 additions & 72 deletions src/components/features/ihv/HardwareProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Single IHV provider selection card (conflicts, install CTAs, local detection).
* Extracted from IHVIntegrationPanel to keep that panel under CodeFactor complexity limits.
*/
import { memo, type ReactNode } from "react";
import { memo, useState, useEffect, type ReactNode } from "react";
import {
Tooltip,
TooltipContent,
Expand Down Expand Up @@ -58,6 +58,7 @@ import {
XCircle,
Globe,
ExternalLink,
ChevronDown,
} from "lucide-react";

export interface HardwareProviderCardProps {
Expand Down Expand Up @@ -123,6 +124,7 @@ function resolveCardChrome(input: {
badgeText: string;
BadgeIcon: typeof CheckCircle | null;
badgeColor: string;
badgeIconColor?: string;
} {
const base =
"relative flex flex-col rounded-xl border p-4.5 transition-all duration-200 cursor-pointer min-w-0 max-w-full overflow-hidden ";
Expand Down Expand Up @@ -227,7 +229,8 @@ function resolveCardChrome(input: {
cardClasses: base + "border-emerald-900/40 bg-emerald-950/10 opacity-95 hover:border-emerald-500/40",
badgeText: "Compatible, runtime available",
BadgeIcon: CheckCircle,
badgeColor: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
badgeColor: "bg-slate-800/60 text-slate-300 border-slate-700/60",
badgeIconColor: "text-emerald-400",
};
}
if (needsPluginInstall && !detectedLocally && !probeLoading) {
Expand Down Expand Up @@ -269,7 +272,8 @@ function resolveCardChrome(input: {
cardClasses: base + "border-slate-800/80 bg-slate-900/40 hover:bg-slate-900 hover:border-slate-700",
badgeText: "Compatible with active passes",
BadgeIcon: CheckCircle,
badgeColor: "bg-emerald-500/10 text-emerald-400 border-emerald-500/15",
badgeColor: "bg-slate-800/60 text-slate-300 border-slate-700/60",
badgeIconColor: "text-emerald-400",
};
}

Expand Down Expand Up @@ -848,14 +852,18 @@ export const HardwareProviderCard = memo(function HardwareProviderCard({
onInstallOrtGpu,
}: HardwareProviderCardProps) {
const isSelected = state.ihvProvider === p.id;
// Selected card starts open so the active target's details aren't hidden behind a click.
const [isExpanded, setIsExpanded] = useState(isSelected);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
useEffect(() => {
setIsExpanded(isSelected);
}, [isSelected]);
const Icon = p.icon;
const pConflicts = getProviderConflicts(p.id, state.passes);
const cardHasCritical = pConflicts.some((c) => c.severity === "critical");
const cardHardwareBlocked =
!isExportTargetProvider(p.id) &&
!isPlatformLocalProvider(p.id) &&
(Boolean(getProviderHardwareBlock(p.id, hardwareProbe)) ||
(p.id === "CPUExecutionProvider" && !hardwareProbe));
Boolean(getProviderHardwareBlock(p.id, hardwareProbe));
const cardBlocked = cardHasCritical || cardHardwareBlocked;
const cardHasWarning = pConflicts.some((c) => c.severity === "warning");
const showSwitchAssist = pConflicts.length > 0 && (isSelected || !cardBlocked);
Expand Down Expand Up @@ -885,7 +893,7 @@ export const HardwareProviderCard = memo(function HardwareProviderCard({
const hardwareCompatibleNotDetected =
p.id === "DmlExecutionProvider" && directMlNeedsInstall && !detectedLocally;

const { cardClasses, badgeText, BadgeIcon, badgeColor } = resolveCardChrome({
const { cardClasses, badgeText, BadgeIcon, badgeColor, badgeIconColor } = resolveCardChrome({
isSelected,
cardBlocked,
cardHardwareBlocked,
Expand All @@ -901,6 +909,7 @@ export const HardwareProviderCard = memo(function HardwareProviderCard({
});
const hardwareDetail = hardwareDetailFor(p.id, hardwareProbe);

const detailsId = `hardware-provider-details-${p.id}`;
const onSelect = () =>
selectProvider(p.id, state, setState, hardwareProbe, detectedProviders, pConflicts, isSelected);

Expand All @@ -926,7 +935,9 @@ export const HardwareProviderCard = memo(function HardwareProviderCard({
<span
className={`inline-flex items-center gap-1 text-[9px] font-mono uppercase tracking-wider font-extrabold px-2 py-0.5 rounded border ${badgeColor}`}
>
{BadgeIcon ? <BadgeIcon className="h-3 w-3" aria-hidden /> : null}
{BadgeIcon ? (
<BadgeIcon className={cn("h-3 w-3", badgeIconColor)} aria-hidden />
) : null}
{badgeText}
</span>
</div>
Expand Down Expand Up @@ -959,71 +970,90 @@ export const HardwareProviderCard = memo(function HardwareProviderCard({
</div>
</TooltipContent>
</Tooltip>
{detectedLocally && hardwareDetail ? (
<p className="text-xs text-emerald-400/90 font-mono break-words">{hardwareDetail}</p>
) : null}
<ProviderPluginInstalls
providerId={p.id}
hardwareProbe={hardwareProbe}
trtRtxNeedsInstall={trtRtxNeedsInstall}
trtNeedsInstall={trtNeedsInstall}
openvinoNeedsInstall={openvinoNeedsInstall}
hardwareInstallBusy={hardwareInstallBusy}
installingTrtRtx={installingTrtRtx}
installTrtRtxError={installTrtRtxError}
installTrtRtxLog={installTrtRtxLog}
onInstallTensorRtRtx={onInstallTensorRtRtx}
installingTrt={installingTrt}
installTrtError={installTrtError}
installTrtLog={installTrtLog}
onInstallTensorRt={onInstallTensorRt}
openvinoInstall={openvinoInstall}
qnnInstall={qnnInstall}
directMlInstall={directMlInstall}
isPreMaxwellBox={isPreMaxwellBox}
cudaNeedsOrtGpuInstall={cudaNeedsOrtGpuInstall}
cudaToolkitMissingAndEpWorks={cudaToolkitMissingAndEpWorks}
cudaToolkitMissing={cudaToolkitMissing}
cudaEpInVenv={cudaEpInVenv}
nvidiaGpus={nvidiaGpus}
installingOrtGpu={installingOrtGpu}
installOrtGpuError={installOrtGpuError}
installOrtGpuLog={installOrtGpuLog}
onInstallOrtGpu={onInstallOrtGpu}
/>
{p.id === "OpenVINOExecutionProvider" ? (
<OpenVinoDeviceHint hardwareProbe={hardwareProbe} />
) : null}
{isWebGpuTarget ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
Not a local Python EP. Select to build web-oriented recipes, then use{" "}
<span className="text-slate-400">Recipe &amp; run → Browser Test</span> / WebGPU benchmark in
Chrome or Edge 113+.
</p>
) : null}
{isExportTarget && !isWebGpuTarget ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
{isLegacyTarget
? "Legacy export path (prefer QNN for Snapdragon). Not available for Studio Execute Live."
: "Export / deploy target only. Not a local Python EP, so Execute Live stays blocked."}
</p>
) : null}
{isPlatformTarget && !detectedLocally && !probeLoading ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
Platform EP: selectable for recipes; Execute Live requires a matching ORT probe hit on this host.
</p>
) : null}
{!detectedLocally &&
!probeLoading &&
!isExportTarget &&
!isPlatformTarget &&
!needsPluginInstall ? (
<p className="text-xs text-slate-600">
{p.id === "CPUExecutionProvider"
? "Hardware detection unavailable. CPU status is unknown."
: "No matching hardware found locally. You can still select for remote/cross-compile targets."}
</p>
) : null}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setIsExpanded((v) => !v);
}}
className="flex items-center gap-1 text-[11px] text-slate-500 hover:text-slate-400 transition-colors cursor-pointer"
aria-expanded={isExpanded}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
aria-controls={detailsId}
>
<ChevronDown
className={cn("h-3 w-3 transition-transform", isExpanded && "rotate-180")}
/>
{isExpanded ? "Hide details" : "Show details"}
</button>
{isExpanded && (
<div id={detailsId}>
{detectedLocally && hardwareDetail ? (
<p className="text-xs text-emerald-400/90 font-mono break-words">{hardwareDetail}</p>
) : null}
<ProviderPluginInstalls
providerId={p.id}
hardwareProbe={hardwareProbe}
trtRtxNeedsInstall={trtRtxNeedsInstall}
trtNeedsInstall={trtNeedsInstall}
openvinoNeedsInstall={openvinoNeedsInstall}
hardwareInstallBusy={hardwareInstallBusy}
installingTrtRtx={installingTrtRtx}
installTrtRtxError={installTrtRtxError}
installTrtRtxLog={installTrtRtxLog}
onInstallTensorRtRtx={onInstallTensorRtRtx}
installingTrt={installingTrt}
installTrtError={installTrtError}
installTrtLog={installTrtLog}
onInstallTensorRt={onInstallTensorRt}
openvinoInstall={openvinoInstall}
qnnInstall={qnnInstall}
directMlInstall={directMlInstall}
isPreMaxwellBox={isPreMaxwellBox}
cudaNeedsOrtGpuInstall={cudaNeedsOrtGpuInstall}
cudaToolkitMissingAndEpWorks={cudaToolkitMissingAndEpWorks}
cudaToolkitMissing={cudaToolkitMissing}
cudaEpInVenv={cudaEpInVenv}
nvidiaGpus={nvidiaGpus}
installingOrtGpu={installingOrtGpu}
installOrtGpuError={installOrtGpuError}
installOrtGpuLog={installOrtGpuLog}
onInstallOrtGpu={onInstallOrtGpu}
/>
{p.id === "OpenVINOExecutionProvider" ? (
<OpenVinoDeviceHint hardwareProbe={hardwareProbe} />
) : null}
{isWebGpuTarget ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
Not a local Python EP. Select to build web-oriented recipes, then use{" "}
<span className="text-slate-400">Recipe &amp; run → Browser Test</span> / WebGPU benchmark in
Chrome or Edge 113+.
</p>
) : null}
{isExportTarget && !isWebGpuTarget ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
{isLegacyTarget
? "Legacy export path (prefer QNN for Snapdragon). Not available for Studio Execute Live."
: "Export / deploy target only. Not a local Python EP, so Execute Live stays blocked."}
</p>
) : null}
{isPlatformTarget && !detectedLocally && !probeLoading ? (
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
Platform EP: selectable for recipes; Execute Live requires a matching ORT probe hit on this host.
</p>
) : null}
{!detectedLocally &&
!probeLoading &&
!isExportTarget &&
!isPlatformTarget &&
!needsPluginInstall ? (
<p className="text-xs text-slate-600">
{p.id === "CPUExecutionProvider"
? "Hardware detection unavailable. CPU status is unknown."
: "No matching hardware found locally. You can still select for remote/cross-compile targets."}
</p>
) : null}
</div>
)}
</div>

<div className="flex items-center justify-center shrink-0">
Expand Down
Loading
Loading