import { Alert, Anchor, Badge, Box, Drawer, Group, Modal, ScrollArea, Stack, Text, Title, Tooltip } from "@mantine/core"; import { useMediaQuery } from "@mantine/hooks"; import { IconArrowUpRight, IconClock, IconFolder, IconHelpCircle, IconHourglass, IconRobot } from "@tabler/icons-react"; import { useCallback, useEffect, useState, type ReactNode } from "react"; import type { Task, SavedUsage } from "../../ui/types"; import { PullRequestInfo, nextRunLabel } from "../../ui/components/CardStatusIcons"; import { OpenChatButton } from "../../ui/components/OpenChatButton"; import { QueuedPrompts } from "../../ui/components/QueuedPrompts"; import { TurnContent } from "../../ui/components/TurnContent"; import { compactTokens, TokenBreakdown, useAnimatedUsage } from "../../ui/components/SavedUsageFormat"; import { GitHubLinks } from "../../ui/components/GitHubLinks"; import { formatWorkedDuration } from "../../ui/time.js"; export function NextTaskDetail({ task, opened, busy, usageLoading = false, error, onClose, onOpenCodex, extraActions, loadImage }: { task: Task | null; opened: boolean; busy: boolean; usageLoading?: boolean; error: string | null; onClose: () => void; onOpenCodex: () => void; extraActions?: ReactNode; loadImage?: (task: Task, url?: string) => Promise; }) { const mobile = useMediaQuery("(max-width: 48em)"); const [lightboxOpen, setLightboxOpen] = useState(false); const [annotationPopups, setAnnotationPopups] = useState>(() => new Set()); const annotationChange = useCallback((id: string, opened: boolean) => { setAnnotationPopups(current => { if (current.has(id) === opened) return current; const next = new Set(current); if (opened) next.add(id); else next.delete(id); return next; }); }, []); const overlayOpen = lightboxOpen || annotationPopups.size > 0; const [now, setNow] = useState(Date.now); useEffect(() => { if (!opened || task?.observed?.lastTurnEvent !== "inProgress") return; setNow(Date.now()); const timer = window.setInterval(() => setNow(Date.now()), 60_000); return () => window.clearInterval(timer); }, [opened, task?.turnId, task?.observed?.lastTurnEvent]); const latestUsage = useAnimatedUsage(task?.sessionUsage?.latest, task?.status === "working", task?.turnId); const totalUsage = useAnimatedUsage(task?.sessionUsage?.total, task?.status === "working", task?.id); if (!task) return null; const start = Date.parse(task.observed?.lastTurnEventAt ?? ""); const duration = task.observed?.lastTurnEvent === "inProgress" && Number.isFinite(start) ? Math.max(0, now - start) : task.observed?.latestTurnDurationMs; const prs = task.detailPullRequests ?? task.pullRequests ?? []; const firstRepo = new Set(); const warnedRepo = new Set(); const deniedRepos = new Set(prs.filter(pr => pr.accessIssue === "denied").map(pr => pr.url.split("/pull/")[0])); const nextRun = task.scheduled && task.nextRunAt && Number.isFinite(Date.parse(task.nextRunAt)) ? nextRunLabel(task.nextRunAt) : null; const count = task.observed?.directChildCount; const source = task.observed?.source === "vscode" ? "Desktop" : task.observed?.source === "cli" ? "CLI" : task.observed?.source === "exec" ? "Exec" : null; const input = task.inputSource === "ordinary" ? "Human" : task.inputSource === "scheduled" ? "Scheduled" : task.inputSource === "unverified" ? "Unknown" : null; const label = task.status === "needs_input" || task.status === "interrupted" ? "Waiting for review" : task.status === "working" ? "Running" : task.status === "completed" ? "Done" : task.statusLabel; const tokenIncomplete = !!task.sessionUsage && [task.sessionUsage.latest, task.sessionUsage.total].some(value => value.partial); const costIncomplete = !!task.sessionUsage && [task.sessionUsage.latest, task.sessionUsage.total].some(value => value.costPartial); const content = {task.project?.name || "No project"} {label && {label}} {task.needsAnswer && Needs answer} {task.status === "interrupted" && Interrupted} {extraActions} {error && {error}} {task.needsAnswer &&
{task.pendingQuestions?.length === 1 ? "Question" : "Questions"}
{(task.pendingQuestions ?? []).map(question => {question.title} {question.options.length > 0 &&
    {question.options.map((option, index) =>
  1. {index + 1}{option}
  2. )}
}
)}
Open chat to answer
}
Technical details {task.turnId && } {source && } {input && }
{(duration != null || nextRun || count != null) &&
Activity {duration != null && }>{task.observed?.lastTurnEvent === "inProgress" ? "Worked for" : "Latest turn worked for"} {formatWorkedDuration(duration)}} {task.observed?.recordedChatDurationMs != null && }>Total chat worked for {formatWorkedDuration(task.observed.recordedChatDurationMs + (task.observed.lastTurnEvent === "inProgress" ? duration ?? 0 : 0))}{task.observed.historicalTimePartial && " · Partial"}} {nextRun && }>{nextRun}} {count != null && }>{count} {count === 1 ? "subagent" : "subagents"}}
}
Usage {task.sessionUsage && (task.sessionUsage.latest.samples > 0 || task.sessionUsage.total.samples > 0) ? <> {(["Latest turn", "Total chat"] as const).map((label, i) => { const value = i === 0 ? latestUsage! : totalUsage!; const raw = i === 0 ? task.sessionUsage!.latest : task.sessionUsage!.total; return ; })}
Tokens{tokenIncomplete && 2}API-equivalent cost1{costIncomplete && ",2"}
{label} }>{value.samples ? `${value.partial ? "≥" : ""}${compactTokens(value.tokens.total_tokens)}` : "—"} {usageCost(value)}
1 Estimates use OpenAI API pricing. Subscription users are not billed this amount. {[task.sessionUsage.latest, task.sessionUsage.total].some(value => value.partial || value.costPartial) && 2 Incomplete estimate: some usage records, turn links, or model prices are missing. Only known usage and prices are included.} : {usageLoading ? "Loading…" : "Usage unavailable."}}
{(prs.length > 0 || task.relatedGitHubLinks?.length) &&
Pull requests {prs.map(pr => { const repo = pr.url.split("/pull/")[0]; const first = !firstRepo.has(repo); firstRepo.add(repo); // A missing PR does not prove the whole repository is inaccessible. const showAccess = !warnedRepo.has(repo) && (deniedRepos.has(repo) ? first : Boolean(pr.accessIssue)); if (showAccess) warnedRepo.add(repo); return ; })}
}
; return mobile ? {content} : {content}; } function usageCost(value: SavedUsage) { if (!value.samples || (value.costPartial && value.costUsd === 0)) return "—"; return `${value.costPartial ? "≥" : ""}$${value.costUsd.toFixed(2)}`; } function ActivityRow({ icon, children }: { icon: ReactNode; children: ReactNode }) { return {icon}{children}; } function TechnicalField({ label, value }: { label: string; value: string }) { return {label}{value}; }