CoDEVX / components /WorkPackageDetail.tsx
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
"use client";
import type { ReactNode } from "react";
import type { WorkPackage } from "@/lib/work-package-types";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { WorkPackageOutputCard } from "@/components/WorkPackageOutput";
import { MarkdownContent } from "@/components/MarkdownContent";
import {
ArrowLeft,
CheckCircle2,
ClipboardCopy,
HelpCircle,
ListTodo,
LoaderCircle,
Play,
SlidersHorizontal,
} from "lucide-react";
import {
getWorkPackageVersion,
getWorkPackageVisualState,
} from "@/lib/work-package-ui";
import { buildPackageQuestionPrompt } from "@/lib/package-chat-context";
function statusLabel(status: WorkPackage["status"]) {
if (status === "todo") return "To do";
if (status === "in_progress") return "In progress";
return "Done";
}
function priorityLabel(priority: WorkPackage["priority"]) {
if (priority === "high") return "High";
if (priority === "medium") return "Medium";
return "Low";
}
function Section(props: {
title: string;
children: ReactNode;
action?: ReactNode;
}) {
return (
<section className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
{props.title}
</div>
{props.action}
</div>
{props.children}
</section>
);
}
function createExcerpt(value: string, maxLength = 220) {
const normalized = value.trim().replace(/\s+/g, " ");
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, maxLength - 1).trimEnd()}...`;
}
function AskAboutButton(props: {
label: string;
onClick: () => void;
}) {
return (
<Button
variant="ghost"
className="h-7 rounded-lg px-2 text-[11px] text-muted-foreground"
aria-label={props.label}
onClick={props.onClick}
>
<HelpCircle className="h-3.5 w-3.5" />
<span className="ml-1">{props.label}</span>
</Button>
);
}
export function WorkPackageDetail(props: {
workPackage?: WorkPackage;
activeWorkPackageId?: string;
onPrefill: (text: string) => void;
onBack: () => void;
}) {
const { workPackage: wp, activeWorkPackageId, onPrefill, onBack } = props;
if (!wp) {
return (
<div className="px-3 pb-3 md:px-4">
<div className="rounded-2xl bg-background/70 px-4 py-4 text-sm text-muted-foreground shadow-sm ring-1 ring-black/5">
Select a work package to inspect its details.
</div>
</div>
);
}
const refName = wp.shortName || wp.title;
const latestOutput = wp.outputs.at(-1);
const visualState = getWorkPackageVisualState({
workPackage: wp,
activeWorkPackageId,
});
const version = getWorkPackageVersion(wp);
const latestOutputExcerpt = latestOutput ? createExcerpt(latestOutput.content) : "";
async function copyReference() {
try {
await navigator.clipboard.writeText(`@${refName} `);
} catch {
// ignore
}
}
return (
<div className="flex h-full min-h-0 flex-col">
<div className="px-3 py-2 md:px-4">
<div className="rounded-2xl bg-background/72 px-3 py-2.5 shadow-sm ring-1 ring-black/5">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-xl"
onClick={onBack}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="min-w-0">
<div className="truncate text-sm font-semibold">{wp.title}</div>
<div className="truncate text-xs text-muted-foreground">
Work package detail view
</div>
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-lg"
onClick={() => onPrefill(`@${refName} ask `)}
>
<HelpCircle className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-lg"
onClick={() => onPrefill(`@${refName} plan `)}
>
<ListTodo className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-lg"
onClick={() => onPrefill(`@${refName} change `)}
>
<SlidersHorizontal className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-lg"
onClick={() => onPrefill(`@${refName} execute `)}
>
<Play className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-lg"
onClick={copyReference}
>
<ClipboardCopy className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-3 md:px-4">
<div className="space-y-3">
<section className="rounded-2xl bg-background/72 px-4 py-4 shadow-sm ring-1 ring-black/5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="text-base font-semibold">{wp.title}</h2>
<div className="mt-1 flex flex-wrap items-center gap-1">
<Badge variant="secondary" className="text-[11px]">
{wp.shortName}
</Badge>
<Badge variant="outline" className="text-[11px]">
{statusLabel(wp.status)}
</Badge>
<Badge variant="outline" className="text-[11px]">
{priorityLabel(wp.priority)}
</Badge>
<Badge variant="outline" className="text-[11px]">
{wp.phase}
</Badge>
{visualState === "running" ? (
<Badge
variant="outline"
className="gap-1 border-amber-300 bg-amber-50 text-[11px] text-amber-700"
>
<LoaderCircle className="h-3 w-3 animate-spin" />
Running
</Badge>
) : null}
{visualState === "done" && version ? (
<Badge
variant="outline"
className="gap-1 border-emerald-300 bg-emerald-50 text-[11px] text-emerald-700"
>
<CheckCircle2 className="h-3 w-3" />
{version}
</Badge>
) : null}
</div>
</div>
</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{wp.objective}
</p>
<div className="mt-3">
<AskAboutButton
label="Ask about objective"
onClick={() =>
onPrefill(
buildPackageQuestionPrompt({
packageRef: refName,
label: "objective",
excerpt: wp.objective,
}),
)
}
/>
</div>
</section>
<Section title="Primary output">
{latestOutput ? (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="text-[11px]">
{latestOutput.title}
</Badge>
<Badge variant="outline" className="text-[11px]">
{latestOutput.executionMode === "real"
? "LLM Automation"
: "Simulated Execution"}
</Badge>
<Badge variant="outline" className="text-[11px]">
{latestOutput.type}
</Badge>
</div>
<div className="rounded-xl bg-muted/20 px-4 py-4">
<MarkdownContent
content={latestOutput.content}
onAskExcerpt={(excerpt) =>
onPrefill(
buildPackageQuestionPrompt({
packageRef: refName,
label: "output excerpt",
excerpt,
}),
)
}
/>
</div>
<div>
<AskAboutButton
label="Ask about latest output"
onClick={() =>
onPrefill(
buildPackageQuestionPrompt({
packageRef: refName,
label: "latest output",
excerpt: latestOutputExcerpt,
}),
)
}
/>
</div>
<div className="rounded-xl bg-muted/32 p-2.5 text-[11px] leading-5 text-muted-foreground">
{latestOutput.disclaimer}
</div>
</div>
) : (
<div className="text-sm leading-6 text-muted-foreground">
No output yet. Run the package to generate its primary artifact.
</div>
)}
</Section>
{wp.outputs.length > 1 ? (
<details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
<summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
Output history
</summary>
<div className="mt-3 space-y-2">
{wp.outputs
.slice(0, -1)
.reverse()
.map((output) => (
<WorkPackageOutputCard
key={output.id}
output={output}
/>
))}
</div>
</details>
) : null}
<details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
<summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
Package context
</summary>
<div className="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
<div className="space-y-3">
<Section title="Expected outputs">
<div className="flex flex-wrap gap-1.5">
{wp.outputFiles.map((output) => (
<Badge key={output} variant="outline" className="text-[11px]">
{output}
</Badge>
))}
</div>
</Section>
{wp.inputFiles.length ? (
<Section title="Expected inputs">
<div className="text-sm leading-6 text-muted-foreground">
{wp.inputFiles.join(", ")}
</div>
</Section>
) : null}
{wp.coreSections.length ? (
<Section title="Core sections">
<div className="text-sm leading-6 text-muted-foreground">
{wp.coreSections.join(", ")}
</div>
</Section>
) : null}
</div>
<Section title="Tasks">
<div className="space-y-2">
{wp.tasks.map((task) => (
<div
key={task.id}
className="rounded-xl bg-muted/28 px-3 py-2.5 text-sm shadow-sm ring-1 ring-black/5"
>
<div className="flex items-start justify-between gap-3">
<div className="font-medium">{task.title}</div>
<AskAboutButton
label="Ask about this task"
onClick={() =>
onPrefill(
buildPackageQuestionPrompt({
packageRef: refName,
label: "task",
excerpt: `${task.title}: ${task.description}`,
}),
)
}
/>
</div>
<div className="mt-1 leading-5 text-muted-foreground">
{task.description}
</div>
</div>
))}
</div>
</Section>
</div>
</details>
</div>
</div>
</div>
);
}