Files
kandev/apps/web/components/task/TaskHeader.tsx
T
2026-08-16 12:08:59 +01:00

100 lines
3.6 KiB
TypeScript

"use client";
/**
* TaskHeader — shared title / identifier / state pill / assignee row.
*
* Pure read; no domain branching. Both the kanban shell (/t/:id) and
* the office shell (/office/tasks/:id) render this above the body.
*
* Designed to be source-agnostic: callers pass plain primitives, so the
* component works for both the kanban Task DTO (lib/types/http) and the
* office Task DTO (app/office/tasks/[id]/types).
*/
import { Badge } from "@kandev/ui/badge";
import type { ForegroundActivity } from "@/lib/types/http";
import { useTranslation } from "react-i18next";
export type TaskHeaderProps = {
identifier?: string | null;
title: string;
state?: string | null;
/**
* Pre-resolved assignee display name. Resolution (initials, agent
* profile name) happens at the call site against the appropriate
* store; this component is a pure renderer.
*/
assigneeName?: string | null;
/** Optional pill colour for the state badge. Falls back to outline. */
stateBadgeVariant?: "default" | "secondary" | "outline" | "destructive";
/**
* Task-level MOST-ACTIVE-WINS activity aggregate.
* When set it takes precedence over the coarse workflow state in the badge, so a
* task still doing background work never reads as a done coarse state and stays
* distinct from a generating task.
*/
foregroundActivity?: ForegroundActivity | null;
/**
* Message-derived "needs me" flags. When set
* the badge reads the waiting variant distinctly ("Permission requested" /
* "Waiting for input") instead of the raw coarse state, matching the sidebar.
*/
hasPendingClarification?: boolean;
hasPendingPermission?: boolean;
};
// Pending input is the most actionable state and sits above live activity, with
// pending permission taking precedence over clarification. Without pending input,
// the task-level activity aggregate sits above the coarse workflow state.
function resolveBadgeLabel(
state: string | null | undefined,
foregroundActivity: ForegroundActivity | null | undefined,
hasPendingClarification: boolean,
hasPendingPermission: boolean,
): string | null {
if (hasPendingPermission) return "task:sessionStatusPermissionRequested";
if (hasPendingClarification) return "task:sessionStatusWaitingForInput";
if (foregroundActivity === "generating") return "task:sessionStatusGenerating";
if (foregroundActivity === "background") return "task:sessionStatusBackgroundRunning";
if (state === "WAITING_FOR_INPUT") return "task:sessionStatusWaitingForInput";
return null;
}
export function TaskHeader({
identifier,
title,
state,
assigneeName,
stateBadgeVariant = "outline",
foregroundActivity,
hasPendingClarification = false,
hasPendingPermission = false,
}: TaskHeaderProps) {
const { t } = useTranslation();
const badgeKey = resolveBadgeLabel(
state,
foregroundActivity,
hasPendingClarification,
hasPendingPermission,
);
// `state` is a workflow-step name the user chose, so it is data and is
// rendered verbatim; only the derived activity badges are copy.
const badgeLabel = badgeKey ? t(badgeKey) : (state ?? null);
return (
<div className="flex items-center gap-3 min-w-0">
{identifier && (
<span className="text-xs font-mono text-muted-foreground shrink-0">{identifier}</span>
)}
<span className="text-sm font-medium truncate flex-1">{title}</span>
{badgeLabel && (
<Badge variant={stateBadgeVariant} className="shrink-0">
{badgeLabel}
</Badge>
)}
{assigneeName && (
<span className="text-xs text-muted-foreground shrink-0">{assigneeName}</span>
)}
</div>
);
}