Files
kandev/apps/web/app/office/tasks/task-row.tsx
T
2026-06-18 18:36:48 +01:00

114 lines
3.8 KiB
TypeScript

"use client";
import { useRouter } from "@/lib/routing/client-router";
import { IconChevronRight, IconLoader2 } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { formatRelativeTime } from "@/lib/utils";
import { useAppStore } from "@/components/state-provider";
import { selectLiveSessionForTask } from "@/lib/state/slices/session/selectors";
import type { OfficeTask } from "@/lib/state/slices/office/types";
import { StatusIcon } from "./status-icon";
import { ExecutionIndicator } from "../components/execution-indicator";
type TaskRowProps = {
task: OfficeTask;
level: number;
hasChildren: boolean;
expanded: boolean;
onToggleExpand: (id: string) => void;
agentName?: string;
};
export function TaskRow({
task,
level,
hasChildren,
expanded,
onToggleExpand,
agentName,
}: TaskRowProps) {
const router = useRouter();
// Show an animated yellow spinner instead of the static status icon
// while any session for this task is RUNNING. Drives the "this task
// is being worked on right now" affordance in the task list.
const isRunning = useAppStore((s) => selectLiveSessionForTask(s, task.id) !== null);
const handleClick = () => {
router.push(`/office/tasks/${task.id}`);
};
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
onToggleExpand(task.id);
};
return (
<div
className="flex items-center gap-2 px-4 py-2.5 text-sm rounded-md hover:bg-muted/60 transition-colors cursor-pointer"
style={{ paddingLeft: `${16 + level * 24}px` }}
onClick={handleClick}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && handleClick()}
>
{hasChildren ? (
<button
onClick={handleToggle}
className="shrink-0 cursor-pointer p-0 border-0 bg-transparent"
aria-label={expanded ? "Collapse" : "Expand"}
>
<IconChevronRight
className={cn(
"h-3.5 w-3.5 text-muted-foreground transition-transform",
expanded && "rotate-90",
)}
/>
</button>
) : (
<span className="w-3.5 shrink-0" />
)}
{isRunning ? (
<IconLoader2
className="h-4 w-4 shrink-0 animate-spin text-yellow-500"
data-testid="task-row-running-spinner"
/>
) : (
<StatusIcon status={task.status} className="h-4 w-4 shrink-0" />
)}
<span className="text-xs text-muted-foreground font-mono shrink-0">{task.identifier}</span>
<span className="flex-1 truncate">{task.title}</span>
{task.isSystem && (
<span className="shrink-0 inline-flex items-center rounded-md border border-border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
System
</span>
)}
{Array.isArray(task.labels) && task.labels.length > 0 && (
<span className="flex items-center gap-1 shrink-0">
{(task.labels as Array<{ name: string; color: string } | string>).map((label) => {
const name = typeof label === "string" ? label : label.name;
const color = typeof label === "string" ? undefined : label.color;
return (
<span
key={name}
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
style={color ? { backgroundColor: `${color}20`, color } : undefined}
>
{name}
</span>
);
})}
</span>
)}
<ExecutionIndicator status={task.status} />
{agentName && (
<span className="text-xs text-muted-foreground shrink-0 truncate max-w-[100px]">
{agentName}
</span>
)}
<span className="text-xs text-muted-foreground shrink-0">
{formatRelativeTime(task.updatedAt)}
</span>
</div>
);
}