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/RunStatusIndicator.tsx b/apps/studio/src/components/RunStatusIndicator.tsx new file mode 100644 index 00000000..28339967 --- /dev/null +++ b/apps/studio/src/components/RunStatusIndicator.tsx @@ -0,0 +1,33 @@ +/** + * RunStatusIndicator — shared live/terminal status badge for Studio-launched + * eval runs. Used anywhere the UI needs the same colored status label and + * active spinner so run/job views stay visually consistent. + */ + +import type { RunStatus } from './stop-run-helpers'; + +export interface RunStatusIndicatorProps { + status: RunStatus; +} + +export function RunStatusIndicator({ status }: RunStatusIndicatorProps) { + const isTerminal = status === 'finished' || 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] ?? 'text-gray-400'; + + return ( + <> + + {status.charAt(0).toUpperCase() + status.slice(1)} + + {!isTerminal && ( + + )} + + ); +} diff --git a/apps/studio/src/components/StopRunButton.tsx b/apps/studio/src/components/StopRunButton.tsx index cc297367..534a7710 100644 --- a/apps/studio/src/components/StopRunButton.tsx +++ b/apps/studio/src/components/StopRunButton.tsx @@ -1,8 +1,9 @@ /** - * StopRunButton — pause-style affordance on /jobs/:runId that interrupts - * a Studio-launched eval. Stop is part of the stop → resume → complete - * workflow, not a destructive cancel: the partial index.jsonl is - * preserved and can be resumed in one click from the run-detail page. + * StopRunButton — stop affordance on /jobs/:runId and active run detail + * views that interrupts a Studio-launched eval. Stop is part of the + * stop → resume → complete workflow, not a destructive cancel: the + * partial index.jsonl is preserved and can be resumed in one click from + * the run-detail page. * * Calls POST /api/eval/run/:id/stop (or the project-scoped variant). * Optimistically flips the local label to "Stopping…" until the next @@ -10,7 +11,7 @@ * point the button hides via `shouldShowStopButton`. * * Styling is intentionally neutral (gray, not red) to signal that this - * is a pause, not a kill. + * stops execution without deleting the partial run workspace. */ import { useState } from 'react'; @@ -51,10 +52,17 @@ export function StopRunButton({ runId, status, isReadOnly, projectId }: StopRunB type="button" onClick={onClick} disabled={stopping} - className="rounded-md border border-gray-700 bg-transparent px-3 py-1.5 text-sm font-medium text-gray-300 hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50" + className="inline-flex items-center gap-2 rounded-md border border-gray-700 bg-transparent px-3 py-1.5 text-sm font-medium text-gray-300 hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50" data-testid="stop-run-button" > - {stopping ? 'Stopping…' : '⏸ Stop'} + {stopping ? ( + 'Stopping…' + ) : ( + <> +