diff --git a/web/src/__tests__/task-detail-mantine.test.tsx b/web/src/__tests__/task-detail-mantine.test.tsx index 5cbfe10a..aa329a13 100644 --- a/web/src/__tests__/task-detail-mantine.test.tsx +++ b/web/src/__tests__/task-detail-mantine.test.tsx @@ -371,7 +371,7 @@ describe('task detail Mantine migration', () => { ); expect(screen.getByRole('button', { name: 'History' })).toBeDefined(); expect(screen.queryByRole('tab', { name: 'Timeline' })).toBeNull(); - expect(screen.getByText('Work View')).toBeDefined(); + expect(screen.getByTestId('task-overview-primary')).toBeDefined(); }); it('translates legacy and versioned deep links into modes and local sections', async () => { diff --git a/web/src/__tests__/task-work-view-mantine.test.tsx b/web/src/__tests__/task-work-view-mantine.test.tsx index 4934b94f..d33d7fdc 100644 --- a/web/src/__tests__/task-work-view-mantine.test.tsx +++ b/web/src/__tests__/task-work-view-mantine.test.tsx @@ -3,6 +3,7 @@ import { cleanup, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { + getTaskOverviewComposition, getTaskReadinessChecks, shouldDefaultTaskDetailToWork, TaskWorkView, @@ -10,6 +11,24 @@ import { import { createMockTask, renderWithProviders } from './test-utils'; import type { WorkProductPreview } from '@veritas-kanban/shared'; +const readyChecks = [ + { + id: 'objective' as const, + label: 'Clear objective', + passed: true, + detail: 'Objective is clear.', + severity: 'required' as const, + }, +]; + +const missingChecks = [ + { + ...readyChecks[0], + passed: false, + detail: 'Add a concrete description of the expected outcome.', + }, +]; + const mocks = vi.hoisted(() => ({ onOpenChat: vi.fn(), onOpenTab: vi.fn(), @@ -82,6 +101,61 @@ function renderWorkView(task = createMockTask()) { ); } +describe('task Overview composition', () => { + it.each([ + { + expected: 'new', + task: createMockTask({ status: 'todo' }), + checks: missingChecks, + options: {}, + }, + { + expected: 'ready', + task: createMockTask({ status: 'todo', type: 'feature' }), + checks: readyChecks, + options: {}, + }, + { + expected: 'active', + task: createMockTask({ status: 'in-progress' }), + checks: readyChecks, + options: { activeRun: true }, + }, + { + expected: 'blocked', + task: createMockTask({ status: 'blocked' }), + checks: missingChecks, + options: {}, + }, + { + expected: 'failed', + task: createMockTask({ status: 'in-progress' }), + checks: readyChecks, + options: { activeRun: false, effectiveAttemptStatus: 'failed' as const }, + }, + { + expected: 'review', + task: createMockTask({ + status: 'in-progress', + attempt: { id: 'attempt-complete', agent: 'codex', status: 'complete' }, + }), + checks: readyChecks, + options: {}, + }, + { + expected: 'done', + task: createMockTask({ status: 'done' }), + checks: missingChecks, + options: {}, + }, + ])( + 'composes the $expected lifecycle state deterministically', + ({ expected, task, checks, options }) => { + expect(getTaskOverviewComposition(task, checks, options).state).toBe(expected); + } + ); +}); + describe('task work view Mantine surface', () => { beforeEach(() => { vi.clearAllMocks(); @@ -103,7 +177,7 @@ describe('task work view Mantine surface', () => { cleanup(); }); - it('surfaces blockers and missing readiness items in one task work view', () => { + it('makes a blocker the dominant state and collapses irrelevant readiness detail', () => { const task = createMockTask({ id: 'task-blocked', title: 'Fix', @@ -118,14 +192,12 @@ describe('task work view Mantine surface', () => { const { baseElement, container } = renderWorkView(task); - expect(screen.getByText('Work View')).toBeDefined(); + expect(screen.getByTestId('task-overview-primary').getAttribute('data-state')).toBe('blocked'); expect(screen.getByRole('button', { name: 'Resolve blocker' })).toBeDefined(); expect(screen.getAllByText('Waiting on security review').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('Readiness Gate')).toBeDefined(); - expect(screen.getByText('Clear objective')).toBeDefined(); - expect(screen.getByText('Add a concrete description of the expected outcome.')).toBeDefined(); + expect(screen.getByText('What is blocking this task')).toBeDefined(); + expect(screen.queryByText('Readiness')).toBeNull(); expect(container.querySelector('.mantine-Paper-root')).toBeDefined(); - expect(container.querySelector('.mantine-Progress-root')).toBeDefined(); expect(baseElement.querySelector('[data-slot="button"]')).toBeNull(); }); @@ -255,13 +327,14 @@ describe('task work view Mantine surface', () => { renderWorkView(task); expect(screen.getByText('Monitor active run')).toBeDefined(); - expect(screen.getByText('Attempt attempt-1')).toBeDefined(); - expect(screen.getByText('Activity Console')).toBeDefined(); + expect(screen.getByText('attempt-1')).toBeDefined(); + expect(screen.getByTestId('task-overview-primary').getAttribute('data-state')).toBe('active'); + expect(screen.getByText('Current execution')).toBeDefined(); expect(screen.getByText('Current step: Running focused tests')).toBeDefined(); expect(screen.getByText('Installing dependencies')).toBeDefined(); expect(screen.getByText('1h 30m')).toBeDefined(); expect(screen.getByText('$0.04')).toBeDefined(); - expect(screen.getByText('Workflow State')).toBeDefined(); + expect(screen.getByText('Workflow execution')).toBeDefined(); expect(screen.getByText('Workflow release-flow v2')).toBeDefined(); expect(screen.getByText('workflow-run-1')).toBeDefined(); expect(screen.getByText('build')).toBeDefined(); @@ -274,10 +347,9 @@ describe('task work view Mantine surface', () => { .getAttribute('href') ).toBe('/runs/run-456'); - await user.click(screen.getByRole('button', { name: 'Open Agent' })); + await user.click(screen.getByRole('button', { name: 'Monitor active run' })); await user.click(screen.getByRole('button', { name: 'Open Workflow' })); await user.click(screen.getByRole('button', { name: 'Work Products' })); - await user.click(screen.getByRole('button', { name: 'Workflow' })); await user.click(screen.getByRole('button', { name: 'Stop active run' })); await user.click(screen.getByRole('button', { name: 'Stop Agent' })); @@ -358,8 +430,8 @@ describe('task work view Mantine surface', () => { }) ); - expect(screen.getAllByText('Failed').length).toBeGreaterThanOrEqual(1); - expect(screen.queryByText('Live')).toBeNull(); + expect(screen.getByText('Needs recovery')).toBeDefined(); + expect(screen.getByText('Latest attempt: Failed')).toBeDefined(); expect(screen.queryByRole('button', { name: 'Stop active run' })).toBeNull(); }); diff --git a/web/src/components/task/TaskWorkView.tsx b/web/src/components/task/TaskWorkView.tsx index 66783d3e..1665c866 100644 --- a/web/src/components/task/TaskWorkView.tsx +++ b/web/src/components/task/TaskWorkView.tsx @@ -14,16 +14,11 @@ import { SimpleGrid, Stack, Text, - ThemeIcon, Tooltip, } from '@mantine/core'; import { AlertTriangle, - Bot, CheckCircle2, - Clock, - ClipboardCheck, - Coins, Download, ExternalLink, Eye, @@ -32,16 +27,14 @@ import { History, MessageSquare, PackageCheck, - Play, - Route, - ShieldCheck, - Square, - Smartphone, - Terminal, RotateCcw, - Workflow, + ShieldCheck, + Smartphone, + Square, + Terminal, Wifi, WifiOff, + Workflow, XCircle, } from 'lucide-react'; import { @@ -67,8 +60,8 @@ import { } from '@/hooks/useWorkflowStats'; import { clientAllowsLocalAgentControls } from '@/lib/client-policy'; import { API_BASE } from '@/lib/config'; -import { ArtifactPreviewModal } from './ArtifactPreviewModal'; import { sanitizeText } from '@/lib/sanitize'; +import { ArtifactPreviewModal } from './ArtifactPreviewModal'; import { RunAccessPanel } from './RunAccessPanel'; export function getTaskReadinessChecks(task: Task, isCodeTask: boolean): TaskReadinessCheck[] { @@ -97,7 +90,34 @@ interface TaskWorkViewProps { onOpenWorkflow: () => void; } -const STATUS_COLORS: Record = { +export type TaskOverviewState = + 'new' | 'ready' | 'active' | 'blocked' | 'failed' | 'review' | 'done' | 'cancelled'; + +export interface TaskOverviewComposition { + state: TaskOverviewState; + title: string; + detail: string; + color: string; + action: { + label: string; + target: TaskWorkViewTarget | 'workflow' | 'chat'; + }; +} + +interface TaskOverviewCompositionOptions { + activeRun?: boolean; + effectiveAttemptStatus?: TaskAttempt['status']; +} + +const TASK_STATUS_LABELS: Record = { + blocked: 'Blocked', + cancelled: 'Cancelled', + done: 'Done', + 'in-progress': 'In progress', + todo: 'To do', +}; + +const TASK_STATUS_COLORS: Record = { blocked: 'red', cancelled: 'gray', done: 'green', @@ -105,15 +125,135 @@ const STATUS_COLORS: Record = { todo: 'gray', }; -function formatDate(value?: string): string { - if (!value) return 'Not recorded'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return 'Not recorded'; +/** + * Composes the Overview's single dominant state from authoritative task data. + * Attempt and review outcomes remain separately labelled so they are not + * misrepresented as task lifecycle values. + */ +export function getTaskOverviewComposition( + task: Task, + readinessChecks: TaskReadinessCheck[], + options: TaskOverviewCompositionOptions = {} +): TaskOverviewComposition { + const activeRun = + options.activeRun ?? (task.attempt?.status === 'running' || task.attempt?.status === 'pending'); + const attemptStatus = options.effectiveAttemptStatus ?? task.attempt?.status; - return new Intl.DateTimeFormat(undefined, { - dateStyle: 'medium', - timeStyle: 'short', - }).format(date); + if (task.status === 'blocked') { + return { + state: 'blocked', + title: 'Blocked', + detail: task.blockedReason?.note || 'This task has an unresolved blocker.', + color: 'red', + action: { label: 'Resolve blocker', target: 'details' }, + }; + } + + if (task.status === 'cancelled') { + return { + state: 'cancelled', + title: 'Cancelled', + detail: 'This task is closed without a completion handoff.', + color: 'gray', + action: { label: 'Review task', target: 'details' }, + }; + } + + if (task.status === 'done') { + return { + state: 'done', + title: 'Done', + detail: 'Execution is complete. Confirm the handoff and retained evidence.', + color: 'green', + action: { label: 'Review handoff', target: 'work-products' }, + }; + } + + if (activeRun) { + return { + state: 'active', + title: 'Run in progress', + detail: 'An execution attempt is active. Monitor its current step before intervening.', + color: 'blue', + action: { label: 'Monitor active run', target: 'agent' }, + }; + } + + if (attemptStatus === 'failed') { + return { + state: 'failed', + title: 'Needs recovery', + detail: 'The latest attempt failed. Inspect its evidence before retrying.', + color: 'red', + action: { label: 'Inspect failed run', target: 'agent' }, + }; + } + + const hasReviewSignal = Boolean( + task.review || task.reviewComments?.length || attemptStatus === 'complete' + ); + if (hasReviewSignal) { + const needsChanges = + task.review?.decision === 'changes-requested' || task.review?.decision === 'rejected'; + const hasUncheckedVerification = task.verificationSteps?.some((step) => !step.checked); + + if (needsChanges) { + return { + state: 'review', + title: 'Changes requested', + detail: task.review?.summary || 'Review requires follow-up before handoff.', + color: 'yellow', + action: { label: 'Address review decision', target: 'review' }, + }; + } + + if (hasUncheckedVerification) { + return { + state: 'review', + title: 'Verification required', + detail: 'Execution finished with verification steps still unchecked.', + color: 'yellow', + action: { label: 'Complete verification', target: 'details' }, + }; + } + + return { + state: 'review', + title: task.review?.decision === 'approved' ? 'Approved for handoff' : 'Ready for review', + detail: 'Execution has an outcome ready for review and handoff.', + color: task.review?.decision === 'approved' ? 'green' : 'violet', + action: { label: 'Review outcome', target: 'review' }, + }; + } + + const missingReadiness = readinessChecks.find((check) => !check.passed); + if (missingReadiness) { + return { + state: 'new', + title: 'Needs preparation', + detail: missingReadiness.detail, + color: 'yellow', + action: { label: 'Fix readiness', target: 'details' }, + }; + } + + if (task.type === 'code' && !task.git?.worktreePath) { + return { + state: 'ready', + title: 'Ready to prepare', + detail: 'The task is defined. Prepare its isolated worktree before execution.', + color: 'teal', + action: { label: 'Prepare worktree', target: 'git' }, + }; + } + + return { + state: 'ready', + title: 'Ready to run', + detail: 'The task has enough context and verification intent to begin execution.', + color: 'teal', + action: { label: 'Start execution', target: 'agent' }, + }; } function formatDurationMs(value?: number): string { @@ -181,79 +321,6 @@ function getAttemptColor(status?: TaskAttempt['status']): string { } } -function getNextAction( - task: Task, - readinessChecks: TaskReadinessCheck[] -): { - label: string; - detail: string; - target: TaskWorkViewTarget | 'workflow' | 'chat'; -} { - if (task.status === 'blocked') { - return { - label: 'Resolve blocker', - detail: task.blockedReason?.note || 'Review the blocker details before starting execution.', - target: 'details', - }; - } - - const missingReadiness = readinessChecks.find((check) => !check.passed); - if (missingReadiness) { - return { - label: 'Fix readiness', - detail: missingReadiness.detail, - target: 'details', - }; - } - - if (task.attempt?.status === 'running' || task.attempt?.status === 'pending') { - return { - label: 'Monitor active run', - detail: 'An agent attempt is active. Watch the live session before changing task state.', - target: 'agent', - }; - } - - if (task.type === 'code' && !task.git?.worktreePath) { - return { - label: 'Prepare worktree', - detail: 'Create a worktree before starting the agent.', - target: 'git', - }; - } - - const uncheckedVerification = task.verificationSteps?.some((step) => !step.checked); - if (uncheckedVerification) { - return { - label: 'Complete verification', - detail: 'There are unchecked verification steps.', - target: 'details', - }; - } - - if (task.review?.decision === 'changes-requested' || task.review?.decision === 'rejected') { - return { - label: 'Address review decision', - detail: task.review.summary || 'Review requires follow-up before handoff.', - target: 'review', - }; - } - - if (task.status === 'done') { - return { - label: 'Review handoff', - detail: 'Task is marked done. Confirm work products and completion evidence.', - target: 'work-products', - }; - } - - return { - label: 'Start or continue execution', - detail: 'Task is ready enough to start the agent or workflow.', - target: 'agent', - }; -} - function getVerificationSummary(task: Task): { complete: number; total: number } { const steps = task.verificationSteps ?? []; return { @@ -278,8 +345,6 @@ function getWorkflowStatusColor(status?: WorkflowRunStatus): string { return 'green'; case 'failed': return 'red'; - case 'pending': - return 'gray'; default: return 'gray'; } @@ -307,15 +372,6 @@ function workflowRunMatchesTask(run: WorkflowRun, taskId: string): boolean { return run.taskId === taskId || contextTaskId === taskId; } -function getWorkflowDurationMs(run?: WorkflowRun): number | undefined { - if (!run?.startedAt) return undefined; - const start = new Date(run.startedAt).getTime(); - if (Number.isNaN(start)) return undefined; - const end = run.completedAt ? new Date(run.completedAt).getTime() : Date.now(); - if (Number.isNaN(end) || end < start) return undefined; - return end - start; -} - function getWorkflowProgress(run?: WorkflowRun): { complete: number; total: number; @@ -324,11 +380,7 @@ function getWorkflowProgress(run?: WorkflowRun): { const steps = run?.steps ?? []; const total = steps.length; const complete = steps.filter((step) => step.status === 'completed').length; - return { - complete, - total, - percent: total > 0 ? Math.round((complete / total) * 100) : 0, - }; + return { complete, total, percent: total > 0 ? Math.round((complete / total) * 100) : 0 }; } function workflowRunSortWeight(status: WorkflowRunStatus): number { @@ -382,8 +434,8 @@ export function TaskWorkView({ isFetching: isAgentStatusFetching, } = useAgentStatus(task.id); const { outputs, isConnected, isRunning } = useAgentStream(task.id, agentStatus?.attemptId); - const { data: activeWorkflowRuns = [], isLoading: activeWorkflowRunsLoading } = useActiveRuns(); - const { data: recentWorkflowRuns = [], isLoading: recentWorkflowRunsLoading } = useRecentRuns(); + const { data: activeWorkflowRuns = [] } = useActiveRuns(); + const { data: recentWorkflowRuns = [] } = useRecentRuns(); const stopAgent = useStopAgent(); const { authContext, hasPermission } = useIdentity(); const canControlAgents = @@ -392,11 +444,6 @@ export function TaskWorkView({ () => evaluateTaskReadiness(task, { isCodeTask }), [task, isCodeTask] ); - const readinessChecks = readinessSummary.checks; - const passedReadiness = readinessSummary.passed; - const readinessPercent = readinessSummary.percent; - const nextAction = getNextAction(task, readinessChecks); - const verification = getVerificationSummary(task); const latestWorkProducts = workProducts.slice(0, 3); const latestOutputs = outputs.slice(-6); const taskAttemptActive = @@ -406,6 +453,10 @@ export function TaskWorkView({ ((agentStatus === undefined || isAgentStatusFetching) && (isRunning || taskAttemptActive)); const effectiveAttemptStatus = !activeRun && taskAttemptActive ? ('failed' as const) : task.attempt?.status; + const overview = getTaskOverviewComposition(task, readinessSummary.checks, { + activeRun, + effectiveAttemptStatus, + }); const retryableRun = effectiveAttemptStatus === 'failed' || effectiveAttemptStatus === 'complete'; const stopControl = agentStatus?.controls?.controls.find((control) => control.action === 'stop'); const canStop = @@ -418,9 +469,8 @@ export function TaskWorkView({ : undefined) ?? stopControl?.reason ?? 'Validated stop capability evidence is not available for this run.'; + const verification = getVerificationSummary(task); const attemptDuration = getAttemptDurationMs(task.attempt); - const trackedTime = formatTrackedSeconds(task.timeTracking?.totalSeconds); - const runCost = formatCost(task.actualCost); const taskWorkflowRuns = useMemo(() => { const byId = new Map(); for (const run of [...activeWorkflowRuns, ...recentWorkflowRuns]) { @@ -430,38 +480,44 @@ export function TaskWorkView({ }, [activeWorkflowRuns, recentWorkflowRuns, task.id]); const workflowRun = taskWorkflowRuns[0]; const workflowProgress = getWorkflowProgress(workflowRun); - const workflowDuration = getWorkflowDurationMs(workflowRun); - const workflowLoading = activeWorkflowRunsLoading || recentWorkflowRunsLoading; const nextActionRestricted = - nextAction.target === 'agent' || (nextAction.target === 'git' && !canControlAgents); - const effectiveNextAction = + overview.action.target === 'agent' || (overview.action.target === 'git' && !canControlAgents); + const effectiveAction = nextActionRestricted && !canControlAgents ? { label: isCodeTask ? 'Review timeline' : 'Review details', - detail: - 'This client can review task state, comments, gates, and run history. Agent execution is unavailable for this client mode.', target: isCodeTask ? ('timeline' as const) : ('details' as const), } - : nextAction; + : overview.action; const currentStep = latestOutputs.length > 0 ? sanitizeText(latestOutputs[latestOutputs.length - 1].content).slice(0, 180) : activeRun ? 'Waiting for agent output.' - : task.attempt?.status === 'failed' - ? 'Last run failed. Open Agent to inspect logs or start another attempt.' + : effectiveAttemptStatus === 'failed' + ? 'The latest run failed. Inspect its timeline before retrying.' : 'No live output is available.'; + const showReadiness = overview.state === 'new' || overview.state === 'ready'; + const showReviewHandoff = Boolean( + overview.state === 'review' || + overview.state === 'done' || + task.review || + task.reviewComments?.length || + task.deliverables?.length || + task.attachments?.length || + workProducts.length + ); const openNextAction = () => { - if (effectiveNextAction.target === 'workflow') { + if (effectiveAction.target === 'workflow') { onOpenWorkflow(); return; } - if (effectiveNextAction.target === 'chat') { + if (effectiveAction.target === 'chat') { onOpenChat(); return; } - onOpenTab(effectiveNextAction.target); + onOpenTab(effectiveAction.target); }; const handleStopAgent = () => { @@ -473,28 +529,39 @@ export function TaskWorkView({ return ( <> - - -
+ + + - - - - Work View - - {task.status} - - - {readinessPercent}% ready + + Task state + + + {TASK_STATUS_LABELS[task.status] ?? task.status} - - {effectiveNextAction.detail} + + {overview.title} -
+ + {overview.detail} + +
{!readOnly && ( - )} @@ -507,485 +574,320 @@ export function TaskWorkView({ )} - {task.attempt?.id && ( - + {overview.state === 'blocked' && ( + } + > + + + {task.blockedReason?.note || 'The task is blocked without a recorded explanation.'} + + {(task.blockedBy?.length ?? 0) > 0 && ( + + {task.blockedBy?.length} blocking task link + {task.blockedBy?.length === 1 ? '' : 's'} recorded. + + )} + + )} - - - - - - - - Live Session + {(task.attempt || activeRun) && ( + + + + + + + Current execution + + Latest attempt: {formatAttemptStatus(effectiveAttemptStatus)} + + + + Current step: {currentStep} + + + Attempt {agentStatus?.attemptId || task.attempt?.id || 'none'} + + + {isConnected ? ( + + ) : ( + + )} + Event stream {isConnected ? 'connected' : 'disconnected'} + + + + + {activeRun && !readOnly && canControlAgents && ( + + )} + {retryableRun && !activeRun && !readOnly && canControlAgents && ( + + )} + {task.attempt?.id && ( + + )} + - - {formatAttemptStatus(effectiveAttemptStatus)} - - {task.attempt ? ( + + +
+ + Duration + + + {formatDurationMs(attemptDuration)} + +
+
+ + Tracked time + + + {formatTrackedSeconds(task.timeTracking?.totalSeconds)} + +
+
+ + Cost + + + {formatCost(task.actualCost)} + +
+
+ + {latestOutputs.length > 0 && ( +
+ + Recent run output ({outputs.length} events) + + + + {latestOutputs.map((output, index) => ( + + {output.type === 'stdin' ? 'You: ' : ''} + {sanitizeText(output.content)} + + ))} + + +
+ )} +
+
+ )} + + {workflowRun && ( + + + + + + Workflow execution + + {getWorkflowStatusLabel(workflowRun.status)} + + - Attempt {task.attempt.id} - - - {task.attempt.agent} - {task.attempt.model ? ` | ${task.attempt.model}` : ''} - {task.attempt.provider ? ` | ${task.attempt.provider}` : ''} - - - Started {formatDate(task.attempt.started)} - - - Duration {formatDurationMs(attemptDuration)} + Workflow {workflowRun.workflowId} v{workflowRun.workflowVersion} - ) : ( - - No agent attempt has been started for this task. - - )} - - {canControlAgents && ( - - )} - {isCodeTask && ( - - )} - {retryableRun && canControlAgents && ( - - )} - - - - - - - - - - - Code and Review - - - - {task.git?.worktreePath - ? 'worktree ready' - : task.git?.repo - ? 'repo set' - : 'not set'} + + + {workflowRun.id} - - - Repo: {task.git?.repo || 'Not configured'} - - - Branch: {task.git?.branch || task.git?.baseBranch || 'Not configured'} - - - Review: {getReviewLabel(task)} - - - {isCodeTask && ( - - )} - {isCodeTask && ( - - )} - - - -
- - - - -
- - - Activity Console - - {activeRun ? 'Live' : formatAttemptStatus(effectiveAttemptStatus)} + {workflowRun.currentStep && ( + + {workflowRun.currentStep} - {isConnected ? ( - - ) : ( - - )} - - {isConnected ? 'Connected' : 'Disconnected'} - - - - Attempt {agentStatus?.attemptId || task.attempt?.id || 'none'} - -
- - {activeRun && !readOnly && canControlAgents && ( - )} - {retryableRun && !activeRun && !readOnly && canControlAgents && ( - - )} - {task.attempt?.id && ( - - )} - -
- - -
- - -
- - Run duration - - - {formatDurationMs(attemptDuration)} - -
-
-
-
- - -
- - Tracked time - - - {trackedTime} - -
-
-
-
- - -
- - Cost - - - {runCost} - -
-
-
-
- -
- - - Current step: {currentStep} - - - {outputs.length} events - - - - - {latestOutputs.length > 0 ? ( - latestOutputs.map((output, index) => ( - - {output.type === 'stdin' ? 'You: ' : ''} - {sanitizeText(output.content)} - - )) - ) : ( - - {activeRun - ? 'Waiting for output...' - : 'Live output will appear here when an agent runs.'} - - )} - - -
-
-
- - - - -
- - - Workflow State - - {getWorkflowStatusLabel(workflowRun?.status)} - - - - {workflowRun - ? workflowRun.automation - ? `Automation ${workflowRun.automation.automationVersionId} ยท Workflow ${workflowRun.workflowId} v${workflowRun.workflowVersion}` - : `Workflow ${workflowRun.workflowId} v${workflowRun.workflowVersion}` - : 'No workflow run has been recorded for this task.'} - -
- -
- - {workflowLoading ? ( - - - Loading workflow state... + Steps {workflowProgress.complete}/{workflowProgress.total} - ) : workflowRun ? ( - - - - {workflowRun.id} - - {workflowRun.currentStep && ( - - {workflowRun.currentStep} - - )} - {workflowRun.automation && ( - - automation v{workflowRun.automation.automationVersion} - - )} - - - - Steps {workflowProgress.complete}/{workflowProgress.total} - - - Duration {formatDurationMs(workflowDuration)} - - - Started {formatDate(workflowRun.startedAt)} - - - - {workflowRun.error && ( - }> - {workflowRun.error} - - )} - - ) : ( - - Open Workflow to start a recipe or inspect available workflow runs. - - )} -
-
- - - - - - - Readiness Gate - - - {passedReadiness}/{readinessChecks.length} checks - - - - - {readinessChecks.map((check) => ( - - {check.passed ? ( - - ) : ( - - )} -
- - {check.label} - - - {check.detail} - -
-
- ))} -
-
-
- - - - - - - - - Handoff - - - - {verification.complete}/{verification.total} verified - - - - Deliverables: {task.deliverables?.length ?? 0} - - - Attachments: {task.attachments?.length ?? 0} - - {task.qaGate?.required && ( - } - > - QA gate {task.qaGate.passed ? 'passed' : 'is required before completion'}. + + {workflowRun.error && ( + }> + {workflowRun.error} )} - - - - + )} - - - + {showReadiness && ( + + + - - - Work Products - + + Readiness - - {workProducts.length} + + {readinessSummary.passed}/{readinessSummary.total} checks + + + {readinessSummary.checks.map((check) => ( + + {check.passed ? ( + + ) : ( + + )} +
+ + {check.label} + + {!check.passed && ( + + {check.detail} + + )} +
+
+ ))} +
+
+
+ )} + + {showReviewHandoff && ( + + + + + + Review and handoff + {task.review?.decision && ( + + Review: {getReviewLabel(task)} + + )} + + + {isCodeTask && ( + + )} + + + + + +
+ + Verification + + + {verification.complete}/{verification.total} complete + +
+
+ + Deliverables + + + {task.deliverables?.length ?? 0} + +
+
+ + Saved work products + + + {workProducts.length} + +
+
+ + {task.qaGate?.required && !task.qaGate.passed && ( + }> + QA gate is required before completion. + + )} + {workProductsLoading ? ( @@ -995,6 +897,12 @@ export function TaskWorkView({ ) : latestWorkProducts.length > 0 ? ( + + + + Latest work products + + {latestWorkProducts.map((product) => { const sourceLink = product.sourceLinks?.find((link) => normalizeSafeHref(link.href) @@ -1005,6 +913,7 @@ export function TaskWorkView({ product.artifact?.state === 'available' ? `${API_BASE}/work-products/${encodeURIComponent(product.id)}/artifact/download?version=${product.version}` : undefined; + return ( @@ -1067,14 +976,68 @@ export function TaskWorkView({ ); })} - ) : ( - - Reports, checklists, completion packets, and evidence summaries will appear here. - - )} + ) : null}
-
+ )} + + {task.attempt?.id && ( +
+ + Technical run and governance evidence + +
+ +
+
+ )} + + {isCodeTask && overview.state === 'ready' && ( + + + + +
+ + Repository context + + + {task.git?.repo || 'Repository not configured'} |{' '} + {task.git?.branch || task.git?.baseBranch || 'Branch not configured'} + +
+
+ +
+
+ )} + + {!isCodeTask && !workflowRun && overview.state === 'ready' && ( + + + + +
+ + Workflow + + + Start a recipe or inspect the workflows available to this task. + +
+
+ +
+
+ )}