import { type ComponentType, type ReactNode, useState } from "react"; import { IconBranch, IconCircleCheck, IconCircleDashed, IconCircleX, IconExternal, IconFileDiff, IconGlobe, IconIssue, IconLoader, IconMerge, IconPullRequest, IconPullRequestClosed, IconX, } from "../../icons"; import { cn } from "../../lib/utils"; import { Button } from "../../ui/button"; export type DetailsPullState = "open" | "draft" | "closed" | "merged"; export type DetailsCheck = { name: string; url?: string; state: "pending" | "passed" | "failed" | "skipped"; }; export type DetailsPull = { number: number; title: string; url: string; state: DetailsPullState; checks: DetailsCheck[]; }; export type DetailsSubagent = { id: string; task: string; model?: string; state: "running" | "done" | "failed" | "stopped"; }; export type DetailsLink = { url: string; label: string; kind: "issue" | "pull" | "web" }; export type DetailsFact = { label: string; value: string; title?: string }; export type SessionDetailsViewProps = { summary?: string; /** Line totals for the chat's work; absent until first checked. */ changes?: { adds: number; dels: number; files: number }; branch?: string; /** `null` once looked up and none exists; absent while unknown or unavailable. */ pull?: DetailsPull | null; subagents?: DetailsSubagent[]; links?: DetailsLink[]; facts?: DetailsFact[]; onDiff?: () => void; onClose: () => void; }; const LINKS = 5; const ROW = "-mx-2 flex min-w-0 items-center gap-2 rounded-md squircle px-2 py-1 text-left text-xs text-foreground outline-none transition-colors focus-visible:outline-2 focus-visible:outline-ring/50"; const ACTIVE = "hover:bg-muted/60 focus-visible:bg-muted/60"; const GLYPH = "size-4 shrink-0"; type Icon = ComponentType<{ className?: string }>; const PULL_ICONS: Record = { open: [IconPullRequest, "text-success"], draft: [IconPullRequest, "text-muted-foreground"], closed: [IconPullRequestClosed, "text-destructive"], merged: [IconMerge, "text-merged"], }; const SUBAGENT_ICONS: Record = { running: [IconLoader, "animate-spin text-muted-foreground"], done: [IconCircleCheck, "text-success"], failed: [IconCircleX, "text-destructive"], stopped: [IconCircleDashed, "text-muted-foreground"], }; const LINK_ICONS: Record = { issue: IconIssue, pull: IconPullRequest, web: IconGlobe, }; function Section({ title, children }: { title: string; children: ReactNode }) { return (

{title}

{children}
); } function External( { href, label, children }: { href: string; label?: string; children: ReactNode }, ) { return ( {children} ); } function Changes( { changes, onDiff }: { changes: NonNullable; onDiff?: () => void; }, ) { const label = `Changes: ${changes.files} ${ changes.files === 1 ? "file" : "files" }, ${changes.adds} additions, ${changes.dels} deletions`; const content = ( <> Changes +{changes.adds} -{changes.dels} ); if (!onDiff) { return
{content}
; } return ( ); } function checkSummary(checks: DetailsCheck[]): [Icon, string, string] | undefined { const counted = checks.filter((check) => check.state !== "skipped"); if (!counted.length) return; const failed = counted.filter((check) => check.state === "failed").length; const pending = counted.filter((check) => check.state === "pending").length; if (failed) { return [ IconCircleX, "text-destructive", `${failed} ${failed === 1 ? "check" : "checks"} failed`, ]; } if (pending) { return [ IconLoader, "animate-spin text-muted-foreground", `Checks running ${counted.length - pending}/${counted.length}`, ]; } return [IconCircleCheck, "text-success", "Checks passed"]; } function Checks( { url, summary: [Icon, color, label] }: { url: string; summary: [Icon, string, string] }, ) { return ( {label} ); } function Pull({ pull }: { pull: DetailsPull }) { const [Icon, color] = PULL_ICONS[pull.state]; // A closed pull request's checks are history; only live ones say anything about the branch. const summary = pull.state === "open" || pull.state === "draft" ? checkSummary(pull.checks) : undefined; return ( <> {pull.title} #{pull.number} {summary && } ); } /** * Details of one channel. Each part shows once its data is known, so a channel with no work yet * shows only what exists. */ export function SessionDetailsView( { summary, changes, branch, pull, subagents, links, facts, onDiff, onClose, }: SessionDetailsViewProps, ) { const [all, setAll] = useState(false); const shown = all ? links : links?.slice(0, LINKS); return ( ); }