From a9783858903f6dc8c6889abdd873b01925d84545 Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 26 May 2026 15:48:54 +1000 Subject: [PATCH 1/4] hide resume actions for active eval runs --- apps/cli/src/commands/results/serve.ts | 2 ++ apps/studio/src/components/ResumeRunActions.tsx | 5 ++++- apps/studio/src/components/resume-run-helpers.test.ts | 10 ++++++++++ apps/studio/src/components/resume-run-helpers.ts | 4 ++++ apps/studio/src/lib/types.ts | 2 ++ .../src/routes/projects/$projectId_/runs/$runId.tsx | 1 + apps/studio/src/routes/runs/$runId.tsx | 1 + 7 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands/results/serve.ts b/apps/cli/src/commands/results/serve.ts index 9eac714d..1cbac71f 100644 --- a/apps/cli/src/commands/results/serve.ts +++ b/apps/cli/src/commands/results/serve.ts @@ -435,10 +435,12 @@ async function handleRunDetail(c: C, { searchDir }: DataContext) { // Studio-side resume against this exact run. Remote runs live in the // results-repo cache and cannot be resumed in place, so omit both fields. const resumeMeta = meta.source === 'local' ? deriveResumeMeta(searchDir, meta.path) : {}; + const liveStatus = meta.source === 'local' ? getActiveRunStatus(meta.path) : undefined; return c.json({ results: stripHeavyFields(loaded), source: meta.source, source_label: meta.displayName, + ...(liveStatus && { status: liveStatus }), ...resumeMeta, }); } catch { diff --git a/apps/studio/src/components/ResumeRunActions.tsx b/apps/studio/src/components/ResumeRunActions.tsx index c7f6f521..b9232444 100644 --- a/apps/studio/src/components/ResumeRunActions.tsx +++ b/apps/studio/src/components/ResumeRunActions.tsx @@ -27,6 +27,7 @@ import { buildResumeRequestBody, shouldShowResumeActions, } from './resume-run-helpers'; +import type { RunStatus } from './stop-run-helpers'; export interface ResumeRunActionsProps { results: EvalResult[]; @@ -36,6 +37,7 @@ export interface ResumeRunActionsProps { projectId?: string; isReadOnly: boolean; plannedTestCount?: number; + runStatus?: RunStatus; } export function ResumeRunActions({ @@ -46,12 +48,13 @@ export function ResumeRunActions({ projectId, isReadOnly, plannedTestCount, + runStatus, }: ResumeRunActionsProps) { const navigate = useNavigate(); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); - if (!shouldShowResumeActions(results, isReadOnly, plannedTestCount)) return null; + if (!shouldShowResumeActions(results, isReadOnly, plannedTestCount, runStatus)) return null; // Both actions need the run dir + the original eval file. Without those // we can't target the existing run workspace, so we render the buttons diff --git a/apps/studio/src/components/resume-run-helpers.test.ts b/apps/studio/src/components/resume-run-helpers.test.ts index 9ceb7187..62ed9441 100644 --- a/apps/studio/src/components/resume-run-helpers.test.ts +++ b/apps/studio/src/components/resume-run-helpers.test.ts @@ -25,6 +25,16 @@ describe('shouldShowResumeActions', () => { expect(shouldShowResumeActions([ok('a'), errored('b')], false)).toBe(true); }); + it('hides while the run is still active even if it looks incomplete', () => { + expect(shouldShowResumeActions([ok('a')], false, 5, 'running')).toBe(false); + expect(shouldShowResumeActions([errored('a')], false, undefined, 'starting')).toBe(false); + }); + + it('shows once the run is terminal and resumable', () => { + expect(shouldShowResumeActions([ok('a')], false, 5, 'failed')).toBe(true); + expect(shouldShowResumeActions([errored('a')], false, undefined, 'finished')).toBe(true); + }); + it('hides in read-only mode even when execution errors are present', () => { expect(shouldShowResumeActions([errored('a')], true)).toBe(false); }); diff --git a/apps/studio/src/components/resume-run-helpers.ts b/apps/studio/src/components/resume-run-helpers.ts index b8e6451f..8cd4e075 100644 --- a/apps/studio/src/components/resume-run-helpers.ts +++ b/apps/studio/src/components/resume-run-helpers.ts @@ -11,6 +11,8 @@ import type { EvalResult, RunEvalRequest } from '~/lib/types'; +import { isTerminalRunStatus, type RunStatus } from './stop-run-helpers'; + export type ResumeMode = 'resume' | 'rerun'; export interface BuildResumeRequestParams { @@ -39,8 +41,10 @@ export function shouldShowResumeActions( results: EvalResult[], isReadOnly: boolean, plannedTestCount?: number, + runStatus?: RunStatus, ): boolean { if (isReadOnly) return false; + if (runStatus && !isTerminalRunStatus(runStatus)) return false; if (results.some((r) => r.executionStatus === 'execution_error')) return true; if (plannedTestCount !== undefined && results.length < plannedTestCount) return true; return false; diff --git a/apps/studio/src/lib/types.ts b/apps/studio/src/lib/types.ts index 595babb0..998daef3 100644 --- a/apps/studio/src/lib/types.ts +++ b/apps/studio/src/lib/types.ts @@ -84,6 +84,8 @@ export interface RunDetailResponse { results: EvalResult[]; source: 'local' | 'remote'; source_label?: string; + /** Live execution status when this run is still tracked in-memory by Studio. */ + status?: 'starting' | 'running' | 'finished' | 'failed'; /** Path to the run workspace directory (relative to cwd when inside, otherwise absolute). Local runs only. */ run_dir?: string; /** Eval file path the run was launched against, if recorded in benchmark.json. Local runs only. */ diff --git a/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx b/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx index 84e5c989..7ce7ee0b 100644 --- a/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx +++ b/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx @@ -78,6 +78,7 @@ function ProjectRunDetailPage() { projectId={projectId} isReadOnly={isReadOnly} plannedTestCount={data?.planned_test_count} + runStatus={data?.status} /> {!isReadOnly && ( {error &&

{error}

} diff --git a/apps/studio/src/routes/jobs/$runId.tsx b/apps/studio/src/routes/jobs/$runId.tsx index 3107b0b8..2ab73192 100644 --- a/apps/studio/src/routes/jobs/$runId.tsx +++ b/apps/studio/src/routes/jobs/$runId.tsx @@ -9,6 +9,7 @@ import { Link, createFileRoute } from '@tanstack/react-router'; +import { RunStatusIndicator } from '~/components/RunStatusIndicator'; import { StopRunButton } from '~/components/StopRunButton'; import { useEvalRunStatus, useStudioConfig } from '~/lib/api'; @@ -45,15 +46,6 @@ function JobDetailPage() { const isTerminal = status.status === 'finished' || status.status === 'failed'; - const statusColors: Record = { - starting: 'text-yellow-400', - running: 'text-cyan-400', - finished: 'text-emerald-400', - failed: 'text-red-400', - }; - - const statusColor = statusColors[status.status] ?? 'text-gray-400'; - return (
@@ -79,12 +71,7 @@ function JobDetailPage() {
- - {status.status.charAt(0).toUpperCase() + status.status.slice(1)} - - {!isTerminal && ( - - )} +
diff --git a/apps/studio/src/routes/projects/$projectId_/jobs/$runId.tsx b/apps/studio/src/routes/projects/$projectId_/jobs/$runId.tsx index 8c509502..a1535ce2 100644 --- a/apps/studio/src/routes/projects/$projectId_/jobs/$runId.tsx +++ b/apps/studio/src/routes/projects/$projectId_/jobs/$runId.tsx @@ -4,6 +4,7 @@ import { Link, createFileRoute } from '@tanstack/react-router'; +import { RunStatusIndicator } from '~/components/RunStatusIndicator'; import { StopRunButton } from '~/components/StopRunButton'; import { useEvalRunStatus, useStudioConfig } from '~/lib/api'; @@ -40,15 +41,6 @@ function ProjectJobDetailPage() { const isTerminal = status.status === 'finished' || status.status === 'failed'; - const statusColors: Record = { - starting: 'text-yellow-400', - running: 'text-cyan-400', - finished: 'text-emerald-400', - failed: 'text-red-400', - }; - - const statusColor = statusColors[status.status] ?? 'text-gray-400'; - return (
@@ -78,12 +70,7 @@ function ProjectJobDetailPage() { isReadOnly={isReadOnly} projectId={projectId} /> - - {status.status.charAt(0).toUpperCase() + status.status.slice(1)} - - {!isTerminal && ( - - )} +
diff --git a/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx b/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx index c6dcc756..e18e267c 100644 --- a/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx +++ b/apps/studio/src/routes/projects/$projectId_/runs/$runId.tsx @@ -8,6 +8,7 @@ import { useState } from 'react'; import { ResumeRunActions } from '~/components/ResumeRunActions'; import { RunDetail } from '~/components/RunDetail'; import { RunEvalModal } from '~/components/RunEvalModal'; +import { RunStatusIndicator } from '~/components/RunStatusIndicator'; import { StopRunButton } from '~/components/StopRunButton'; import { useProjectRunDetail, useStudioConfig } from '~/lib/api'; @@ -92,6 +93,7 @@ function ProjectRunDetailPage() { runStatus={runStatus} /> )} + {runStatus && } {!isReadOnly && !isActiveRun && (