/** * Single row inside the agent-runs formSheet route * (`app/(app)/[workspace]/issue/[id]/runs.tsx`). Same component for active * and past tasks — * the trailing Cancel button is conditional on `status in {queued, * dispatched, running}`, and the status badge / colour swaps based on the * AgentTask.status enum. * * Tapping a past row is a no-op in v1 — the transcript-detail screen is * explicitly out of scope per /Users/qingnaiyuan/.claude/plans/ * ok-plan-linked-taco.md. */ import { Alert, Pressable, View } from "react-native"; import type { AgentTask, TaskFailureReason } from "@multica/core/types"; import { Text } from "@/components/ui/text"; import { ActorAvatar } from "@/components/ui/actor-avatar"; import { useCancelTask } from "@/data/mutations/issues"; import { useActorLookup } from "@/data/use-actor-name"; import { timeAgo } from "@/lib/time-ago"; interface Props { task: AgentTask; issueId: string; } const ACTIVE_STATUSES: readonly AgentTask["status"][] = [ "queued", "dispatched", "running", ]; export function RunRow({ task, issueId }: Props) { const { getName } = useActorLookup(); const isActive = ACTIVE_STATUSES.includes(task.status); const summary = task.trigger_summary?.trim() || fallbackSummary(task); // Past tasks use completed_at when present (server fills it for terminal // statuses); active tasks fall back to created_at so the user sees how // long it's been waiting. const timestamp = task.completed_at || task.created_at; return ( {getName("agent", task.agent_id)} · {summary} {timestamp ? timeAgo(timestamp) : ""} {isActive ? : null} ); } function StatusBadge({ task }: { task: AgentTask }) { const label = STATUS_LABEL[task.status] ?? task.status; const cls = STATUS_CLASS[task.status] ?? "text-muted-foreground"; // For failed tasks, surface the failure_reason inline so users don't have // to drill in. Reasons are coarse enums; missing/empty stays as just "Failed". if (task.status === "failed" && task.failure_reason) { const reasonLabel = FAILURE_REASON_LABEL[task.failure_reason]; if (reasonLabel) { return ( {label} · {reasonLabel} ); } } return {label}; } function CancelButton({ taskId, issueId, }: { taskId: string; issueId: string; }) { const mutation = useCancelTask(issueId); const onPress = () => { Alert.alert( "Cancel task?", "The agent will stop after the current step.", [ { text: "Keep running", style: "cancel" }, { text: "Cancel task", style: "destructive", onPress: () => mutation.mutate(taskId), }, ], ); }; return ( Cancel ); } function fallbackSummary(task: AgentTask): string { switch (task.kind) { case "comment": return "Comment task"; case "autopilot": return "Autopilot run"; case "chat": return "Chat task"; case "quick_create": return "Quick create"; case "direct": default: return "Task"; } } const STATUS_LABEL: Record = { queued: "Queued", dispatched: "Starting", waiting_local_directory: "Waiting for directory", running: "Running", completed: "Done", failed: "Failed", cancelled: "Cancelled", }; const STATUS_CLASS: Record = { queued: "text-muted-foreground", dispatched: "text-brand", waiting_local_directory: "text-muted-foreground", running: "text-brand", completed: "text-muted-foreground", failed: "text-destructive", cancelled: "text-muted-foreground", }; const FAILURE_REASON_LABEL: Record = { agent_error: "Agent error", timeout: "Timeout", codex_semantic_inactivity: "Codex inactivity", runtime_offline: "Runtime offline", runtime_recovery: "Runtime recovery", manual: "Manual", };