From d4366127e9726ff7b2055967d2fa476da3705dc9 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 4 Feb 2026 14:36:47 -0800 Subject: [PATCH 1/2] more --- src/components/StatusBadge.tsx | 4 +- src/screens/BenchmarkJobDetailScreen.tsx | 59 ++++- src/screens/BenchmarkRunDetailScreen.tsx | 263 +++++++++++++++++++---- 3 files changed, 273 insertions(+), 53 deletions(-) diff --git a/src/components/StatusBadge.tsx b/src/components/StatusBadge.tsx index a5277d1d..57b8725b 100644 --- a/src/components/StatusBadge.tsx +++ b/src/components/StatusBadge.tsx @@ -133,8 +133,8 @@ export const getStatusDisplay = (status: string): StatusDisplay => { return { icon: figures.tick, color: colors.success, - text: "COMPLETED ", - label: "Completed", + text: "COMPLETE ", + label: "Complete", }; case "canceled": return { diff --git a/src/screens/BenchmarkJobDetailScreen.tsx b/src/screens/BenchmarkJobDetailScreen.tsx index 76b49747..cd992e7f 100644 --- a/src/screens/BenchmarkJobDetailScreen.tsx +++ b/src/screens/BenchmarkJobDetailScreen.tsx @@ -17,6 +17,7 @@ import { type ResourceOperation, } from "../components/ResourceDetailPage.js"; import { getBenchmarkJob } from "../services/benchmarkJobService.js"; +import { getBenchmarkRun } from "../services/benchmarkService.js"; import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; @@ -35,6 +36,7 @@ export function BenchmarkJobDetailScreen({ const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [fetchedJob, setFetchedJob] = React.useState(null); + const [runNames, setRunNames] = React.useState>(new Map()); // Find job in store first const jobFromStore = benchmarkJobs.find((j) => j.id === benchmarkJobId); @@ -66,6 +68,47 @@ export function BenchmarkJobDetailScreen({ // Use fetched job for full details, fall back to store for basic display const job = fetchedJob || jobFromStore; + // Fetch run names when job is loaded + React.useEffect(() => { + if (!job) return; + + const runIds: string[] = []; + + // Collect run IDs from outcomes + if (job.benchmark_outcomes) { + job.benchmark_outcomes.forEach((outcome) => { + runIds.push(outcome.benchmark_run_id); + }); + } + + // Collect run IDs from in-progress runs + if (job.in_progress_runs) { + job.in_progress_runs.forEach((run) => { + if (!runIds.includes(run.benchmark_run_id)) { + runIds.push(run.benchmark_run_id); + } + }); + } + + // Fetch run details for each run ID + Promise.all( + runIds.map(async (runId) => { + try { + const run = await getBenchmarkRun(runId); + return { id: runId, name: run.name || runId }; + } catch { + return { id: runId, name: runId }; + } + }) + ).then((results) => { + const namesMap = new Map(); + results.forEach((result) => { + namesMap.set(result.id, result.name); + }); + setRunNames(namesMap); + }); + }, [job]); + // Show loading state if (!job && benchmarkJobId && !error) { return ( @@ -496,24 +539,24 @@ export function BenchmarkJobDetailScreen({ } // Collect benchmark run IDs for operations - const benchmarkRunIds: { id: string; agentName: string }[] = []; + const benchmarkRunIds: { id: string; name: string }[] = []; if (job.benchmark_outcomes) { job.benchmark_outcomes.forEach((outcome) => { + // Use fetched run name from state, fallback to run ID + const runName = runNames.get(outcome.benchmark_run_id) || outcome.benchmark_run_id; benchmarkRunIds.push({ id: outcome.benchmark_run_id, - agentName: outcome.agent_name, + name: runName, }); }); } if (job.in_progress_runs) { job.in_progress_runs.forEach((run) => { - let agentName = "Unknown Agent"; - if (run.agent_config && "name" in run.agent_config) { - agentName = (run.agent_config as any).name; - } // Avoid duplicates if (!benchmarkRunIds.find((r) => r.id === run.benchmark_run_id)) { - benchmarkRunIds.push({ id: run.benchmark_run_id, agentName }); + // Use fetched run name from state, fallback to run ID + const runName = runNames.get(run.benchmark_run_id) || run.benchmark_run_id; + benchmarkRunIds.push({ id: run.benchmark_run_id, name: runName }); } }); } @@ -525,7 +568,7 @@ export function BenchmarkJobDetailScreen({ benchmarkRunIds.slice(0, 9).forEach((run, idx) => { operations.push({ key: `view-run-${idx}`, - label: `View Run: ${run.agentName}`, + label: `View Run: ${run.name}`, color: colors.info, icon: figures.arrowRight, shortcut: String(idx + 1), diff --git a/src/screens/BenchmarkRunDetailScreen.tsx b/src/screens/BenchmarkRunDetailScreen.tsx index 7a48a6c5..21d87b8b 100644 --- a/src/screens/BenchmarkRunDetailScreen.tsx +++ b/src/screens/BenchmarkRunDetailScreen.tsx @@ -3,12 +3,13 @@ * Uses the generic ResourceDetailPage component */ import React from "react"; -import { Text } from "ink"; +import { Box, Text } from "ink"; import figures from "figures"; import { useNavigation } from "../store/navigationStore.js"; import { useBenchmarkStore, type BenchmarkRun, + type ScenarioRun, } from "../store/benchmarkStore.js"; import { ResourceDetailPage, @@ -16,10 +17,16 @@ import { type DetailSection, type ResourceOperation, } from "../components/ResourceDetailPage.js"; -import { getBenchmarkRun } from "../services/benchmarkService.js"; +import { getBenchmarkRun, listScenarioRuns } from "../services/benchmarkService.js"; import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; +import { getStatusDisplay, StatusBadge } from "../components/StatusBadge.js"; +import { + Table, + createTextColumn, + createComponentColumn, +} from "../components/Table.js"; import { colors } from "../utils/theme.js"; interface BenchmarkRunDetailScreenProps { @@ -35,6 +42,8 @@ export function BenchmarkRunDetailScreen({ const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [fetchedRun, setFetchedRun] = React.useState(null); + const [scenarioRuns, setScenarioRuns] = React.useState([]); + const [scenarioRunsLoading, setScenarioRunsLoading] = React.useState(false); // Find run in store first const runFromStore = benchmarkRuns.find((r) => r.id === benchmarkRunId); @@ -42,6 +51,19 @@ export function BenchmarkRunDetailScreen({ // Polling function const pollRun = React.useCallback(async () => { if (!benchmarkRunId) return null as unknown as BenchmarkRun; + + // Also refresh scenario runs when polling + listScenarioRuns({ + limit: 10, + benchmarkRunId, + }) + .then((result) => { + setScenarioRuns(result.scenarioRuns); + }) + .catch(() => { + // Silently fail for scenario runs + }); + return getBenchmarkRun(benchmarkRunId); }, [benchmarkRunId]); @@ -63,9 +85,52 @@ export function BenchmarkRunDetailScreen({ } }, [benchmarkRunId, loading, fetchedRun]); + // Fetch scenario runs for this benchmark run + React.useEffect(() => { + if (benchmarkRunId && !scenarioRunsLoading && scenarioRuns.length === 0) { + setScenarioRunsLoading(true); + + listScenarioRuns({ + limit: 10, // Show up to 10 scenarios + benchmarkRunId, + }) + .then((result) => { + setScenarioRuns(result.scenarioRuns); + setScenarioRunsLoading(false); + }) + .catch(() => { + // Silently fail for scenario runs - not critical + setScenarioRunsLoading(false); + }); + } + }, [benchmarkRunId, scenarioRunsLoading, scenarioRuns.length]); + // Use fetched run for full details, fall back to store for basic display const run = fetchedRun || runFromStore; + // Auto-refresh scenario runs every 5 seconds if benchmark run is running + React.useEffect(() => { + if (!benchmarkRunId || !run) return; + + // Only refresh if run is still running + if (run.state !== "running") return; + + const interval = setInterval(() => { + listScenarioRuns({ + limit: 10, + benchmarkRunId, + }) + .then((result) => { + setScenarioRuns(result.scenarioRuns); + }) + .catch(() => { + // Silently fail + }); + }, 5000); + + return () => clearInterval(interval); + }, [benchmarkRunId, run]); + // Show loading state if (!run && benchmarkRunId && !error) { return ( @@ -123,6 +188,70 @@ export function BenchmarkRunDetailScreen({ ); } + // Helper to calculate overall run status based on scenarios + const calculateOverallStatus = (scenarios: ScenarioRun[]): { + status: "failed" | "pass" | "in-progress" | "not-started"; + label: string; + color: string; + icon: string; + } => { + if (scenarios.length === 0) { + return { + status: "not-started", + label: "Not Started", + color: colors.textDim, + icon: figures.circle, + }; + } + + // Check for any failures or timeouts + const hasFailed = scenarios.some( + (s) => s.state === "failed" || s.state === "timeout" + ); + if (hasFailed) { + return { + status: "failed", + label: "Failed", + color: colors.error, + icon: figures.cross, + }; + } + + // Check if all are completed + const allCompleted = scenarios.every( + (s) => s.state === "completed" || s.state === "scored" + ); + if (allCompleted) { + return { + status: "pass", + label: "Complete", + color: colors.success, + icon: figures.tick, + }; + } + + // Check if any are running + const anyRunning = scenarios.some( + (s) => s.state === "running" || s.state === "scoring" + ); + if (anyRunning) { + return { + status: "in-progress", + label: "In Progress", + color: colors.warning, + icon: figures.circleFilled, + }; + } + + // Default to not started + return { + status: "not-started", + label: "Not Started", + color: colors.textDim, + icon: figures.circle, + }; + }; + // Helper to format duration const formatDuration = (ms: number): string => { if (ms < 1000) return `${ms}ms`; @@ -173,6 +302,95 @@ export function BenchmarkRunDetailScreen({ }); } + // Overall Status Section + const overallStatus = calculateOverallStatus(scenarioRuns); + detailSections.push({ + title: "Overall Status", + icon: overallStatus.icon, + color: overallStatus.color, + fields: [ + { + label: "Status", + value: ( + + {overallStatus.label} + + ), + }, + { + label: "Scenarios", + value: `${scenarioRuns.length} scenario${scenarioRuns.length !== 1 ? "s" : ""}`, + }, + ], + }); + + // Scenario Runs Section + if (scenarioRuns.length > 0) { + // Define columns for scenario table + const scenarioColumns = [ + createTextColumn("id", "ID", (s: ScenarioRun) => s.id, { + width: 26, + color: colors.idColor, + dimColor: false, + bold: false, + }), + createTextColumn("name", "Name", (s: ScenarioRun) => s.name || "(unnamed)", { + width: 50, + }), + createComponentColumn( + "status", + "Status", + (s, _index, isSelected) => { + const statusDisplay = getStatusDisplay(s.state); + const text = statusDisplay.text.slice(0, 12).padEnd(12, " "); + return ( + + {text} + + ); + }, + { width: 12 }, + ), + createTextColumn( + "score", + "Score", + (s: ScenarioRun) => { + const score = s.scoring_contract_result?.score; + return score !== undefined ? String(score) : ""; + }, + { + width: 10, + color: colors.info, + }, + ), + ]; + + detailSections.push({ + title: "Scenario Runs", + icon: figures.pointer, + color: colors.info, + fields: [ + { + label: "", + value: ( + + s.id} + /> + + ), + }, + ], + }); + } + // Timing section const timingFields = []; if (run.start_time_ms) { @@ -207,26 +425,6 @@ export function BenchmarkRunDetailScreen({ }); } - // Environment Variables section - if ( - run.environment_variables && - Object.keys(run.environment_variables).length > 0 - ) { - const envFields = Object.entries(run.environment_variables).map( - ([key, value]) => ({ - label: key, - value: {value}, - }), - ); - - detailSections.push({ - title: "Environment Variables", - icon: figures.info, - color: colors.success, - fields: envFields, - }); - } - // Secrets Provided section (show keys only, not values) if (run.secrets_provided && Object.keys(run.secrets_provided).length > 0) { const secretFields = Object.entries(run.secrets_provided).map( @@ -369,27 +567,6 @@ export function BenchmarkRunDetailScreen({ } lines.push( ); - // Environment Variables - if ( - r.environment_variables && - Object.keys(r.environment_variables).length > 0 - ) { - lines.push( - - Environment Variables - , - ); - Object.entries(r.environment_variables).forEach(([key, value], idx) => { - lines.push( - - {" "} - {key}: {value} - , - ); - }); - lines.push( ); - } - // Secrets Provided if (r.secrets_provided && Object.keys(r.secrets_provided).length > 0) { lines.push( From e67626fb2c6d4dc3ed764d406f33eaf29150b34f Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 4 Feb 2026 14:37:08 -0800 Subject: [PATCH 2/2] cp --- src/screens/BenchmarkJobDetailScreen.tsx | 12 +++++++---- src/screens/BenchmarkRunDetailScreen.tsx | 26 ++++++++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/screens/BenchmarkJobDetailScreen.tsx b/src/screens/BenchmarkJobDetailScreen.tsx index cd992e7f..add0243d 100644 --- a/src/screens/BenchmarkJobDetailScreen.tsx +++ b/src/screens/BenchmarkJobDetailScreen.tsx @@ -36,7 +36,9 @@ export function BenchmarkJobDetailScreen({ const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [fetchedJob, setFetchedJob] = React.useState(null); - const [runNames, setRunNames] = React.useState>(new Map()); + const [runNames, setRunNames] = React.useState>( + new Map(), + ); // Find job in store first const jobFromStore = benchmarkJobs.find((j) => j.id === benchmarkJobId); @@ -99,7 +101,7 @@ export function BenchmarkJobDetailScreen({ } catch { return { id: runId, name: runId }; } - }) + }), ).then((results) => { const namesMap = new Map(); results.forEach((result) => { @@ -543,7 +545,8 @@ export function BenchmarkJobDetailScreen({ if (job.benchmark_outcomes) { job.benchmark_outcomes.forEach((outcome) => { // Use fetched run name from state, fallback to run ID - const runName = runNames.get(outcome.benchmark_run_id) || outcome.benchmark_run_id; + const runName = + runNames.get(outcome.benchmark_run_id) || outcome.benchmark_run_id; benchmarkRunIds.push({ id: outcome.benchmark_run_id, name: runName, @@ -555,7 +558,8 @@ export function BenchmarkJobDetailScreen({ // Avoid duplicates if (!benchmarkRunIds.find((r) => r.id === run.benchmark_run_id)) { // Use fetched run name from state, fallback to run ID - const runName = runNames.get(run.benchmark_run_id) || run.benchmark_run_id; + const runName = + runNames.get(run.benchmark_run_id) || run.benchmark_run_id; benchmarkRunIds.push({ id: run.benchmark_run_id, name: runName }); } }); diff --git a/src/screens/BenchmarkRunDetailScreen.tsx b/src/screens/BenchmarkRunDetailScreen.tsx index 21d87b8b..3ac57931 100644 --- a/src/screens/BenchmarkRunDetailScreen.tsx +++ b/src/screens/BenchmarkRunDetailScreen.tsx @@ -17,7 +17,10 @@ import { type DetailSection, type ResourceOperation, } from "../components/ResourceDetailPage.js"; -import { getBenchmarkRun, listScenarioRuns } from "../services/benchmarkService.js"; +import { + getBenchmarkRun, + listScenarioRuns, +} from "../services/benchmarkService.js"; import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; @@ -189,7 +192,9 @@ export function BenchmarkRunDetailScreen({ } // Helper to calculate overall run status based on scenarios - const calculateOverallStatus = (scenarios: ScenarioRun[]): { + const calculateOverallStatus = ( + scenarios: ScenarioRun[], + ): { status: "failed" | "pass" | "in-progress" | "not-started"; label: string; color: string; @@ -206,7 +211,7 @@ export function BenchmarkRunDetailScreen({ // Check for any failures or timeouts const hasFailed = scenarios.some( - (s) => s.state === "failed" || s.state === "timeout" + (s) => s.state === "failed" || s.state === "timeout", ); if (hasFailed) { return { @@ -219,7 +224,7 @@ export function BenchmarkRunDetailScreen({ // Check if all are completed const allCompleted = scenarios.every( - (s) => s.state === "completed" || s.state === "scored" + (s) => s.state === "completed" || s.state === "scored", ); if (allCompleted) { return { @@ -232,7 +237,7 @@ export function BenchmarkRunDetailScreen({ // Check if any are running const anyRunning = scenarios.some( - (s) => s.state === "running" || s.state === "scoring" + (s) => s.state === "running" || s.state === "scoring", ); if (anyRunning) { return { @@ -334,9 +339,14 @@ export function BenchmarkRunDetailScreen({ dimColor: false, bold: false, }), - createTextColumn("name", "Name", (s: ScenarioRun) => s.name || "(unnamed)", { - width: 50, - }), + createTextColumn( + "name", + "Name", + (s: ScenarioRun) => s.name || "(unnamed)", + { + width: 50, + }, + ), createComponentColumn( "status", "Status",