"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) => (
))}
);
}