/**
* 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",
};