feat(web): consolidate task attempts in History mode (#1348)

This commit is contained in:
Brad Groux 2026-09-02 23:00:07 -05:00 • committed by GitHub
parent 0779a11079
commit 8eb306a19e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 10 deletions

View file

@ -506,7 +506,11 @@ describe('agent run timeline Mantine surface', () => {
/>
);
expect(screen.getByText('Run Timeline')).toBeDefined();
expect(screen.getByText('Attempt Timeline')).toBeDefined();
expect(screen.getByText('Latest attempt')).toBeDefined();
expect(screen.getByRole('combobox', { name: 'Attempt' }).getAttribute('value')).toBe(
'Latest | attempt-1 | complete'
);
expect(screen.getByText('Stored replay')).toBeDefined();
expect(screen.getByText('command.completed')).toBeDefined();
expect(screen.getByText('stream.stdout')).toBeDefined();
@ -538,6 +542,29 @@ describe('agent run timeline Mantine surface', () => {
expect(mocks.onOpenTab).toHaveBeenCalledWith('work-products');
});
it('distinguishes an earlier attempt from the latest task execution', () => {
const taskWithHistory = createMockTask({
...task,
attempt: {
id: 'attempt-current',
agent: 'veritas',
status: 'running',
started: '2026-06-01T11:00:00.000Z',
},
attempts: task.attempt ? [task.attempt] : [],
});
renderWithProviders(
<AgentRunTimelinePanel task={taskWithHistory} initialAttemptId="attempt-1" />
);
expect(screen.getByText('Historical attempt')).toBeDefined();
expect(screen.getByRole('combobox', { name: 'Attempt' }).getAttribute('value')).toBe(
'Historical | attempt-1 | completed'
);
expect(screen.queryByText(/live polling/)).toBeNull();
});
it('highlights a deep-linked telemetry event', () => {
renderWithProviders(
<AgentRunTimelinePanel

View file

@ -116,7 +116,19 @@ vi.mock('@/components/task/AgentPanel', () => ({
}));
vi.mock('@/components/task/AgentRunTimelinePanel', () => ({
AgentRunTimelinePanel: () => <div>Run timeline panel</div>,
AgentRunTimelinePanel: ({
initialAttemptId,
initialEventId,
}: {
initialAttemptId?: string | null;
initialEventId?: string | null;
}) => (
<div>
Run timeline panel
<span>Attempt target: {initialAttemptId ?? 'none'}</span>
<span>Event target: {initialEventId ?? 'none'}</span>
</div>
),
}));
vi.mock('@/components/task/DiffViewer', () => ({
@ -537,7 +549,11 @@ describe('task detail Mantine migration', () => {
task={codeTask}
open
onOpenChange={mocks.onOpenChange}
navigationTarget={{ tab: 'timeline', timelineAttemptId: 'attempt-1' }}
navigationTarget={{
tab: 'timeline',
timelineAttemptId: 'attempt-1',
timelineEventId: 'event-1',
}}
/>
);
@ -549,6 +565,8 @@ describe('task detail Mantine migration', () => {
expect(screen.getByRole('tab', { name: 'Timeline' }).getAttribute('aria-selected')).toBe(
'true'
);
expect(screen.getByText('Attempt target: attempt-1')).toBeDefined();
expect(screen.getByText('Event target: event-1')).toBeDefined();
rerender(
<TaskDetailPanel

View file

@ -899,12 +899,17 @@ function getRunOptions(
attempts.set(id, label);
};
const latestAttemptLabel =
task.attempt?.status === 'running' || task.attempt?.status === 'pending' ? 'Current' : 'Latest';
addAttempt(
task.attempt?.id,
`${latestAttemptLabel} | ${task.attempt?.id} | ${task.attempt?.status ?? 'unknown'}`
);
for (const trace of traces) {
addAttempt(trace.traceId, `${trace.traceId} (${trace.status})`);
addAttempt(trace.traceId, `Historical | ${trace.traceId} | ${trace.status}`);
}
addAttempt(task.attempt?.id, `${task.attempt?.id} (${task.attempt?.status ?? 'current'})`);
for (const attempt of task.attempts ?? []) {
addAttempt(attempt.id, `${attempt.id} (${attempt.status})`);
addAttempt(attempt.id, `Historical | ${attempt.id} | ${attempt.status}`);
}
for (const event of telemetryEvents) {
addAttempt(getEventAttemptId(event), `${getEventAttemptId(event)} (telemetry)`);
@ -1222,6 +1227,17 @@ export function AgentRunTimelinePanel({
(product) => !selectedAttemptId || product.sourceRunId === selectedAttemptId
);
const pendingTaskApprovals = approvals.filter((approval) => approval.taskId === task.id);
const selectedIsLatestAttempt = Boolean(
selectedAttemptId && selectedAttemptId === task.attempt?.id
);
const selectedIsLiveAttempt = hasLiveAttempt && selectedIsLatestAttempt;
const selectedAttemptScope = selectedIsLiveAttempt
? 'Current attempt'
: selectedIsLatestAttempt
? 'Latest attempt'
: selectedAttemptId
? 'Historical attempt'
: 'Derived history';
useEffect(() => {
setVisibleCount(TIMELINE_PAGE_SIZE);
@ -1436,7 +1452,8 @@ export function AgentRunTimelinePanel({
<ThemeIcon size="sm" radius="xl" variant="light">
<History className="h-4 w-4" />
</ThemeIcon>
<Text fw={700}>Run Timeline</Text>
<Text fw={700}>Attempt Timeline</Text>
<Badge variant="outline">{selectedAttemptScope}</Badge>
<Badge color={SOURCE_COLORS[source]} variant="light">
{source === 'live' ? 'Live' : source === 'stored' ? 'Stored replay' : 'Derived'}
</Badge>
@ -1451,7 +1468,7 @@ export function AgentRunTimelinePanel({
</Group>
<Text size="sm" c="dimmed" mt={6}>
{selectedAttemptId || 'No attempt selected'} | {events.length} events
{hasLiveAttempt ? ' | live polling' : ''}
{selectedIsLiveAttempt ? ' | live polling' : ''}
</Text>
</div>
{isLoading && (
@ -1466,8 +1483,8 @@ export function AgentRunTimelinePanel({
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
<Select
label="Run"
aria-label="Run"
label="Attempt"
aria-label="Attempt"
value={selectedAttemptId}
onChange={setSelectedAttemptId}
data={runOptions}

View file

@ -83,6 +83,15 @@ describe('task workspace navigation', () => {
resolveTaskDetailNavigationTab({ workspace: { version: 1, mode: 'run', section } }, tabs)
).toBe(section);
}
for (const section of ['timeline', 'metrics'] as const) {
expect(resolveTaskDetailNavigationTab({ tab: section }, tabs)).toBe(section);
expect(
resolveTaskDetailNavigationTab(
{ workspace: { version: 1, mode: 'history', section } },
tabs
)
).toBe(section);
}
const tabsWithWorktree = getAvailableTaskDetailTabMetadata({
isCodeTask: true,
hasWorktree: true,