"use client"; import { useMemo } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@kandev/ui/tabs"; import { useAppStore } from "@/components/state-provider"; import { agentTint } from "@/app/office/components/agent-avatar"; import { TaskChat } from "./task-chat"; import { TaskActivity } from "./task-activity"; import { AdvancedChatPanel } from "@/app/office/tasks/[id]/advanced-panels/chat-panel"; import { groupSessionsForTimeline, isGroupLive, isOfficeGroup, type SessionGroup, } from "./session-groups"; import { ApprovalActionBar } from "./components/approval-action-bar"; import type { Task, TaskActivityEntry, TaskComment, TaskSession, TimelineEvent, } from "@/app/office/tasks/[id]/types"; // TAB_TRIGGER_BASE adds a 1px ring to the active tab on top of // shadcn's default bg/text active styling, so the selected tab is // unambiguous across Chat / Activity / agent triggers without // depending on subtle bg contrast. const TAB_TRIGGER_BASE = "cursor-pointer data-[state=active]:ring-1 data-[state=active]:ring-border"; function AgentTabTrigger({ group }: { group: SessionGroup }) { const live = isGroupLive(group); const agentProfileId = group.representative.agentProfileId; // Resolve the agent's display name + role from the office store so a // rename flows through automatically and we never fall back to the // UUID that lands in `session.agentName` when the session's profile // snapshot is empty. const resolved = useAppStore((s) => agentProfileId ? s.office.agentProfiles.find((a) => a.id === agentProfileId) : undefined, ); const label = resolved?.name || group.representative.agentName || "Agent"; // Apply the per-agent tint only when the tab is active, so the // selected state reads clearly. Inactive tabs inherit shadcn's muted // styling — same as Chat / Activity — with a small dot in the agent // color hinting at identity. const activeTint = agentTint(label) .split(/\s+/) .filter(Boolean) .map((c) => `data-[state=active]:${c}`) .join(" "); return ( {label} {live && ( )} ); } // agentDot returns just the background color class from the agent // tint, so inactive tabs can show a tiny color dot for identification // without bleeding the full tint into the tab background. function agentDot(label: string): string { const bg = agentTint(label) .split(/\s+/) .find((c) => c.startsWith("bg-")); return bg ? bg.replace("/15", "") : "bg-muted-foreground/40"; } function AgentTabContent({ taskId, group }: { taskId: string; group: SessionGroup }) { // Pick the active session if any; otherwise fall back to the // representative (most-recent). Mirrors `SessionTimelineEntry`. const active = group.group.find((s) => s.state === "RUNNING"); const embedSessionId = active?.id ?? group.representative.id; return (
); } type ChatActivityTabsProps = { task: Task; comments: TaskComment[]; timeline?: TimelineEvent[]; activity: TaskActivityEntry[]; sessions: TaskSession[]; scrollParent: HTMLElement | null; readOnly: boolean; onCommentsChanged?: () => void; }; /** * Tab strip below the task properties: `Chat | Activity | | | …`. * * Office task sessions (one per (task, agent)) become per-agent sibling * tabs whose content embeds the agent's session transcript via * `AdvancedChatPanel`. Kanban / quick-chat sessions still render inline * inside the Chat tab via `TaskChat`. */ export function ChatActivityTabs({ task, comments, timeline, activity, sessions, scrollParent, readOnly, onCommentsChanged, }: ChatActivityTabsProps) { const officeGroups = useMemo( () => groupSessionsForTimeline(sessions, task.reviewers, task.approvers).filter(isOfficeGroup), [sessions, task.reviewers, task.approvers], ); return ( Chat Activity {officeGroups.map((g) => ( ))} {officeGroups.map((g) => ( ))} ); }