- 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
238 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
};
|