Files
zopu-code/repos/web/components/work-os/work-unit-detail.tsx
-Puter cc47007fa9 Slice 1 polish: archive dormant code, merge work-os into primitives, add futures
- Archive dormant apps (daemon, desktop, native, tui) and superseded
  packages/server into repos/ for reference
- Archive 21 dead web components (chat page shell, work-os cluster, strays)
  into repos/web/; keep reused message-rendering primitives in components/chat
- Merge @code/work-os into @code/primitives; work.ts absorbed via ./work
  subpath and barrel export; update all four importers
- Add docs/futures.md tracking audio/video (blocked at Flue + provider),
  web layout cleanup, and message rendering quality
- Repoint dev:zopu script to @code/agents (the live Flue server)
- Note repos/ reference-code convention in AGENTS.md
2026-07-27 16:34:17 +05:30

238 lines
7.4 KiB
TypeScript

import { Button } from "@code/ui/components/button";
import {
ArrowUpRight,
Bot,
ExternalLink,
FileText,
GitPullRequest,
Layers,
Radio,
TriangleAlert,
} from "lucide-react";
import { formatRelativeTime } from "@/lib/work-os/work-unit-projection";
import type { WorkUnitDetail as WorkUnitDetailData } from "@/lib/work-os/work-unit-projection";
const formatStatus = (
status: WorkUnitDetailData["issue"]["status"]
): string => {
if (status === "needs-input") {
return "Needs input";
}
if (status === "working") {
return "In progress";
}
return status.charAt(0).toUpperCase() + status.slice(1);
};
interface WorkUnitDetailProps {
readonly detail: WorkUnitDetailData;
readonly onOpenPullRequest: (url: string) => void;
readonly onStart: () => void;
readonly startPending: boolean;
}
const Section = ({
children,
icon,
title,
}: {
readonly children: React.ReactNode;
readonly icon: React.ReactNode;
readonly title: string;
}) => (
<section className="rounded-2xl border border-border/30 bg-card/30 p-4">
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
{icon}
{title}
</div>
{children}
</section>
);
export const WorkUnitDetail = ({
detail,
onOpenPullRequest,
onStart,
startPending,
}: WorkUnitDetailProps) => {
const { issue } = detail;
const canStart = issue.status === "open" || issue.status === "failed";
return (
<div className="space-y-4">
{/* Objective */}
<section className="rounded-2xl border border-border/40 bg-card/40 p-5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-[11px] font-medium text-muted-foreground">
Work Unit #{issue.number}
</p>
<h2 className="mt-1 text-lg font-semibold leading-7 tracking-tight">
{issue.title}
</h2>
</div>
<span className="shrink-0 rounded-full bg-muted px-2.5 py-1 text-[11px] font-medium">
{formatStatus(issue.status)}
</span>
</div>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{issue.body}
</p>
{canStart ? (
<Button
className="mt-4 w-full"
disabled={startPending}
onClick={onStart}
type="button"
>
<Bot className="size-4" />
{startPending ? "Starting" : "Start work"}
</Button>
) : null}
</section>
{/* Needs input alert */}
{detail.needsInput ? (
<div className="flex items-start gap-3 rounded-2xl border border-amber-500/30 bg-amber-500/10 p-4">
<TriangleAlert className="mt-0.5 size-4 shrink-0 text-amber-400" />
<div>
<p className="text-sm font-medium text-amber-300">
This work unit needs your input
</p>
<p className="mt-1 text-xs leading-5 text-amber-400/70">
Resolve the open question or decision to unblock the agent.
</p>
</div>
</div>
) : null}
{/* Latest summary */}
{detail.latestSummary ? (
<Section icon={<ArrowUpRight className="size-3" />} title="Latest">
<p className="text-sm leading-6">{detail.latestSummary}</p>
</Section>
) : null}
{/* Linked Signals — authenticated via signalIssueAttachments */}
<Section
icon={<Radio className="size-3" />}
title={`Signals (${detail.signalCount})`}
>
{detail.linkedSignals.length > 0 ? (
<div className="space-y-3">
{detail.linkedSignals.map((signal) => (
<div key={String(signal.signalId)}>
<p className="text-sm font-medium">{signal.title}</p>
<p className="mt-0.5 line-clamp-2 text-xs leading-5 text-muted-foreground">
{signal.summary}
</p>
<p className="mt-1 text-[10px] text-muted-foreground/60">
{signal.sourceCount} source
{signal.sourceCount === 1 ? "" : "s"} ·{" "}
{formatRelativeTime(signal.createdAt)}
</p>
</div>
))}
</div>
) : (
<p className="text-xs leading-5 text-muted-foreground">
No Signals linked to this Work Unit.
</p>
)}
</Section>
{/* Activity timeline */}
<Section
icon={<Layers className="size-3" />}
title={`Timeline (${detail.stepCount} steps)`}
>
{detail.activity.length > 0 ? (
<div className="space-y-4">
{detail.activity.map((item) => (
<div
className="flex gap-3"
key={`${item.kind}-${item.createdAt}`}
>
<div className="mt-1.5 size-1.5 shrink-0 rounded-full bg-foreground/40" />
<div className="min-w-0">
<p className="text-xs font-medium">{item.label}</p>
<p className="mt-0.5 text-xs leading-5 text-muted-foreground">
{item.detail}
</p>
<p className="mt-0.5 text-[10px] text-muted-foreground/60">
{item.time}
</p>
</div>
</div>
))}
</div>
) : (
<p className="text-xs text-muted-foreground">
No activity recorded yet.
</p>
)}
</Section>
{/* Artifacts */}
<Section
icon={<FileText className="size-3" />}
title={`Artifacts (${detail.artifactCount})`}
>
{detail.artifactPaths.length > 0 ? (
<div className="flex flex-wrap gap-2">
{detail.artifactPaths.map((path) => (
<span
className="inline-flex items-center gap-1.5 rounded-lg bg-muted/60 px-2.5 py-1 text-[11px]"
key={path}
>
<FileText className="size-3 text-muted-foreground" />
{path}
</span>
))}
</div>
) : (
<p className="text-xs text-muted-foreground">
No artifacts produced yet.
</p>
)}
</Section>
{/* Pull request */}
{detail.hasPullRequest ? (
<Section
icon={<GitPullRequest className="size-3" />}
title="Pull request"
>
{detail.pullRequestUrl ? (
<div className="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-3">
<p className="text-sm font-medium text-emerald-300">
{detail.pullRequestNumber
? `PR #${detail.pullRequestNumber} ready for review`
: "Pull request ready"}
</p>
<Button
className="mt-3"
onClick={() =>
detail.pullRequestUrl &&
onOpenPullRequest(detail.pullRequestUrl)
}
size="sm"
type="button"
variant="outline"
>
<ExternalLink className="size-3.5" />
Review changes
</Button>
</div>
) : (
<p className="text-xs text-muted-foreground">
A pull request was opened but the URL is unavailable.
</p>
)}
</Section>
) : null}
</div>
);
};