- 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
132 lines
3.9 KiB
TypeScript
132 lines
3.9 KiB
TypeScript
import { Badge } from "@code/ui/components/badge";
|
|
import {
|
|
Activity,
|
|
ChevronRight,
|
|
FileText,
|
|
GitPullRequest,
|
|
Layers,
|
|
Radio,
|
|
TriangleAlert,
|
|
} from "lucide-react";
|
|
|
|
import { formatRelativeTime } from "@/lib/work-os/work-unit-projection";
|
|
import type { WorkUnitCard as WorkUnitCardData } from "@/lib/work-os/work-unit-projection";
|
|
|
|
const STATUS_LABEL: Record<WorkUnitCardData["status"], string> = {
|
|
completed: "Completed",
|
|
failed: "Failed",
|
|
"needs-input": "Needs input",
|
|
open: "Ready",
|
|
queued: "Queued",
|
|
working: "In progress",
|
|
};
|
|
|
|
const STATUS_TONE: Record<WorkUnitCardData["status"], string> = {
|
|
completed: "bg-emerald-500/15 text-emerald-400",
|
|
failed: "bg-destructive/15 text-destructive",
|
|
"needs-input": "bg-amber-500/15 text-amber-400",
|
|
open: "bg-muted text-muted-foreground",
|
|
queued: "bg-violet-500/15 text-violet-400",
|
|
working: "bg-blue-500/15 text-blue-400",
|
|
};
|
|
|
|
interface IndicatorProps {
|
|
readonly icon: React.ReactNode;
|
|
readonly label: string;
|
|
readonly value: number;
|
|
readonly tone?: string;
|
|
}
|
|
|
|
const Indicator = ({ icon, label, tone, value }: IndicatorProps) => (
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
<span className={tone}>{icon}</span>
|
|
{value}
|
|
<span className="sr-only">{label}</span>
|
|
</span>
|
|
);
|
|
|
|
interface WorkUnitCardProps {
|
|
readonly card: WorkUnitCardData;
|
|
readonly selected: boolean;
|
|
readonly onSelect: () => void;
|
|
}
|
|
|
|
export const WorkUnitCard = ({
|
|
card,
|
|
onSelect,
|
|
selected,
|
|
}: WorkUnitCardProps) => (
|
|
<button
|
|
className={`w-full cursor-pointer rounded-2xl border p-4 text-left transition-all duration-200 ${
|
|
selected
|
|
? "border-foreground/20 bg-foreground/5"
|
|
: "border-border/40 bg-card/40 hover:border-border/70 hover:bg-card/60"
|
|
}`}
|
|
onClick={onSelect}
|
|
type="button"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-[11px] font-medium text-muted-foreground">
|
|
#{card.number}
|
|
</p>
|
|
<h3 className="mt-0.5 text-sm font-semibold leading-5 tracking-tight">
|
|
{card.title}
|
|
</h3>
|
|
</div>
|
|
<Badge
|
|
className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE[card.status]}`}
|
|
variant="secondary"
|
|
>
|
|
{STATUS_LABEL[card.status]}
|
|
</Badge>
|
|
</div>
|
|
|
|
<p className="mt-2 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
{card.summary}
|
|
</p>
|
|
|
|
<div className="mt-3 flex flex-wrap items-center gap-3 border-t border-border/30 pt-3">
|
|
{card.currentActivity ? (
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
<Activity className="size-3" />
|
|
{card.currentActivity}
|
|
</span>
|
|
) : null}
|
|
<Indicator
|
|
icon={<Radio className="size-3" />}
|
|
label="signals"
|
|
value={card.signalCount}
|
|
/>
|
|
<Indicator
|
|
icon={<Layers className="size-3" />}
|
|
label="steps"
|
|
value={card.stepCount}
|
|
/>
|
|
<Indicator
|
|
icon={<FileText className="size-3" />}
|
|
label="artifacts"
|
|
value={card.artifactCount}
|
|
/>
|
|
{card.hasPullRequest ? (
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-emerald-400">
|
|
<GitPullRequest className="size-3" />
|
|
PR
|
|
</span>
|
|
) : null}
|
|
{card.needsInput ? (
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-amber-400">
|
|
<TriangleAlert className="size-3" />
|
|
Input needed
|
|
</span>
|
|
) : null}
|
|
<span className="ml-auto inline-flex items-center gap-1">
|
|
<span className="text-[11px] text-muted-foreground/70">
|
|
{formatRelativeTime(card.updatedAt)}
|
|
</span>
|
|
<ChevronRight className="size-3.5 text-muted-foreground/50" />
|
|
</span>
|
|
</div>
|
|
</button>
|
|
);
|