Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 64 additions & 22 deletions apps/dashboard/src/components/ResumeRunActions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* ResumeRunActions — header buttons for resuming an interrupted run.
* ResumeRunActions — run-detail Actions menu for resuming an interrupted run.
*
* Surfaces the existing CLI resume mechanics (`--resume`, `--rerun-failed`)
* via the launch endpoint when the loaded run contains at least one result
Expand All @@ -17,13 +17,14 @@
*/

import { useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';

import { launchEvalRun } from '~/lib/api';
import type { EvalResult } from '~/lib/types';

import {
type ResumeMode,
buildResumeActionMenuItems,
buildResumeRequestBody,
shouldShowResumeActions,
} from './resume-run-helpers';
Expand Down Expand Up @@ -51,10 +52,11 @@ export function ResumeRunActions({
runStatus,
}: ResumeRunActionsProps) {
const navigate = useNavigate();
const menuRef = useRef<HTMLDivElement | null>(null);
const [busy, setBusy] = useState<ResumeMode | null>(null);
const [error, setError] = useState<string | null>(null);

if (!shouldShowResumeActions(results, isReadOnly, plannedTestCount, runStatus)) return null;
const [open, setOpen] = useState(false);
const showActions = shouldShowResumeActions(results, isReadOnly, plannedTestCount, runStatus);

// 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
Expand All @@ -66,9 +68,36 @@ export function ResumeRunActions({
: !suiteFilter
? 'Original eval file path missing from summary.json — cannot determine what to resume'
: '';
const menuItems = buildResumeActionMenuItems({ ready, busy, disabledReason });

useEffect(() => {
if (!open) return;

function handlePointerDown(event: PointerEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}

function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
setOpen(false);
}
}

document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [open]);

if (!showActions) return null;

async function launch(mode: ResumeMode) {
if (!ready || !runDir || !suiteFilter) return;
setOpen(false);
setBusy(mode);
setError(null);
try {
Expand All @@ -89,28 +118,41 @@ export function ResumeRunActions({
}

return (
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => launch('resume')}
disabled={!ready || busy !== null}
title={!ready ? disabledReason : 'Skip already-completed tests, run the rest'}
className="rounded-md bg-amber-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-amber-500 disabled:cursor-not-allowed disabled:opacity-50"
data-testid="resume-run-button"
>
{busy === 'resume' ? 'Resuming…' : '↻ Resume run'}
</button>
<div className="flex flex-col items-end gap-1" ref={menuRef}>
<div className="relative">
<button
type="button"
onClick={() => launch('rerun')}
disabled={!ready || busy !== null}
title={!ready ? disabledReason : 'Re-run failed/errored tests, keep passing results'}
className="rounded-md border border-amber-600/60 bg-transparent px-3 py-1.5 text-sm font-medium text-amber-300 hover:bg-amber-950/40 disabled:cursor-not-allowed disabled:opacity-50"
data-testid="rerun-failed-button"
onClick={() => setOpen((value) => !value)}
disabled={busy !== null}
aria-haspopup="menu"
aria-expanded={open}
className="rounded-md border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm font-medium text-gray-200 hover:border-gray-600 hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
data-testid="run-actions-menu-button"
>
{busy === 'rerun' ? 'Re-running' : 'Rerun failed cases'}
{busy === 'resume' ? 'Resuming...' : busy === 'rerun' ? 'Re-running...' : 'Actions'}
</button>
{open && (
<div
className="absolute left-0 z-20 mt-2 w-48 overflow-hidden rounded-md border border-gray-700 bg-gray-950 py-1 text-sm shadow-xl shadow-black/30 sm:left-auto sm:right-0"
role="menu"
aria-label="Run actions"
>
{menuItems.map((item) => (
<button
key={item.mode}
type="button"
role="menuitem"
onClick={() => void launch(item.mode)}
disabled={item.disabled}
title={item.title}
className="block w-full px-3 py-2 text-left text-gray-200 hover:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-500"
data-testid={item.testId}
>
{item.label}
</button>
))}
</div>
)}
</div>
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/RunEvalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export function RunEvalModal({ open, onClose, projectId, prefill }: RunEvalModal
const canLaunch = !!(suiteFilter.trim() || testIds.length > 0);

return (
<ModalShell onClose={onClose} title="Run Eval">
<ModalShell onClose={onClose} title="Start">
<div className="space-y-4">
{/* Suite filter */}
<div>
Expand Down Expand Up @@ -413,7 +413,7 @@ export function RunEvalModal({ open, onClose, projectId, prefill }: RunEvalModal
disabled={!canLaunch || launching}
className="rounded-md bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 disabled:opacity-50"
>
{launching ? 'Launching…' : 'Run Now'}
{launching ? 'Launching…' : 'Start'}
</button>
</div>
</div>
Expand Down
35 changes: 33 additions & 2 deletions apps/dashboard/src/components/resume-run-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { describe, expect, it } from 'bun:test';

import type { EvalResult } from '~/lib/types';

import { buildResumeRequestBody, shouldShowResumeActions } from './resume-run-helpers';
import {
buildResumeActionMenuItems,
buildResumeRequestBody,
shouldShowResumeActions,
} from './resume-run-helpers';

const ok = (testId: string): EvalResult => ({
testId,
Expand Down Expand Up @@ -78,7 +82,7 @@ describe('buildResumeRequestBody', () => {
});
});

it('builds a rerun-failed request with rerun_failed:true (and no resume key)', () => {
it('builds a rerun-failed request with rerun_failed:true and no resume key', () => {
const body = buildResumeRequestBody({
mode: 'rerun',
runDir: 'runs/r1',
Expand Down Expand Up @@ -108,3 +112,30 @@ describe('buildResumeRequestBody', () => {
});
});
});

describe('buildResumeActionMenuItems', () => {
it('renders concise action labels in the requested order', () => {
expect(
buildResumeActionMenuItems({
ready: true,
busy: null,
disabledReason: '',
}).map((item) => item.label),
).toEqual(['Re-run', 'Resume']);
});

it('keeps unavailable actions visible but disabled with an explanation', () => {
const items = buildResumeActionMenuItems({
ready: false,
busy: null,
disabledReason: 'Run directory unavailable',
});

expect(items[0]?.mode).toBe('rerun');
expect(items[0]?.disabled).toBe(true);
expect(items[0]?.title).toBe('Run directory unavailable');
expect(items[1]?.mode).toBe('resume');
expect(items[1]?.disabled).toBe(true);
expect(items[1]?.title).toBe('Run directory unavailable');
});
});
38 changes: 38 additions & 0 deletions apps/dashboard/src/components/resume-run-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ export interface BuildResumeRequestParams {
target?: string;
}

export interface BuildResumeActionMenuItemsParams {
ready: boolean;
busy: ResumeMode | null;
disabledReason: string;
}

export interface ResumeActionMenuItem {
mode: ResumeMode;
label: string;
title: string;
disabled: boolean;
testId: string;
}

/**
* Whether the resume actions should be visible. The button is shown when:
* 1. At least one recorded row has `execution_status: execution_error`, OR
Expand Down Expand Up @@ -68,3 +82,27 @@ export function buildResumeRequestBody(params: BuildResumeRequestParams): RunEva
}
return body;
}

export function buildResumeActionMenuItems(
params: BuildResumeActionMenuItemsParams,
): ResumeActionMenuItem[] {
const disabled = !params.ready || params.busy !== null;
return [
{
mode: 'rerun',
label: 'Re-run',
title: params.ready
? 'Re-run failed or missing work, keep passing results'
: params.disabledReason,
disabled,
testId: 'rerun-failed-menu-item',
},
{
mode: 'resume',
label: 'Resume',
title: params.ready ? 'Resume missing or errored work' : params.disabledReason,
disabled,
testId: 'resume-run-menu-item',
},
];
}
4 changes: 2 additions & 2 deletions apps/dashboard/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ function ProjectsDashboard() {
onClick={() => setShowRunEval(true)}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-500"
>
▶ Run Eval
Start
</button>
<button
type="button"
Expand Down Expand Up @@ -322,7 +322,7 @@ function SingleProjectHome() {
onClick={() => setShowRunEval(true)}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-500"
>
▶ Run Eval
Start
</button>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/routes/projects/$projectId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ProjectHomePage() {
onClick={() => setShowRunEval(true)}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-500"
>
▶ Run Eval
Start
</button>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function ProjectRunDetailPage() {
onClick={() => setShowRunEval(true)}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-500"
>
▶ Run evals
Start
</button>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/routes/runs/$runId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function RunDetailPage() {
onClick={() => setShowRunEval(true)}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-500"
>
▶ Run evals
Start
</button>
)}
</div>
Expand Down
Loading