"use client"; import { useCallback, useMemo } from "react"; import { IconMessagePlus, IconStar } from "@tabler/icons-react"; import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from "@kandev/ui/dropdown-menu"; import { useAppStore } from "@/components/state-provider"; import { useDockviewStore } from "@/lib/state/dockview-store"; import { useTaskSessions } from "@/hooks/use-task-sessions"; import { addSessionPanel } from "@/lib/state/dockview-panel-actions"; import { getSessionStateIcon } from "@/lib/ui/state-icons"; import { AgentLogo } from "@/components/agent-logo"; import type { TaskSession } from "@/lib/types/http"; import type { AgentProfileOption } from "@/lib/state/slices"; type AgentInfo = { label: string; agentName: string }; function resolveAgentInfo( session: TaskSession, profilesById: Record, ): AgentInfo { const profile = session.agent_profile_id ? profilesById[session.agent_profile_id] : null; if (!profile) return { label: "Unknown agent", agentName: "" }; const parts = profile.label.split(" \u2022 "); return { label: parts[1] || parts[0] || profile.label, agentName: profile.agent_name }; } /** * Renders session items inside the + dropdown menu. * Each item shows session number, agent label, primary star, and state icon. * Clicking focuses an existing tab or re-opens a closed one. */ export function SessionReopenMenuItems({ taskId, groupId, onNewSession, }: { taskId: string; groupId?: string; /** * Click handler for the leading "New Agent" item rendered as the * first row under the section label. Omit to hide the row. */ onNewSession?: () => void; }) { const { sessions } = useTaskSessions(taskId); const api = useDockviewStore((s) => s.api); const centerGroupId = useDockviewStore((s) => s.centerGroupId); const agentProfiles = useAppStore((s) => s.agentProfiles.items); const primarySessionId = useAppStore((s) => { const task = s.kanban.tasks.find((t: { id: string }) => t.id === taskId); return task?.primarySessionId ?? null; }); const profilesById = useMemo( () => Object.fromEntries(agentProfiles.map((p: AgentProfileOption) => [p.id, p])), [agentProfiles], ); const sortedSessions = useMemo( () => [...sessions].sort( (a, b) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime(), ), [sessions], ); const handleClick = useCallback( (sessionId: string, label: string, groupId?: string) => { if (!api) return; // Reopening a session within the same task = same env, so the env switch // action no-ops naturally. We just create the chat panel. addSessionPanel(api, groupId ?? centerGroupId, sessionId, label); }, [api, centerGroupId], ); // Render the section even when there are no sessions yet — the leading // "New Agent" row should still be reachable from the menu. Hide the // whole block only when neither the create handler nor any sessions // are present (e.g. unmounted contexts). if (sortedSessions.length === 0 && !onNewSession) return null; return ( <> Agents {onNewSession && ( New Agent )} {sortedSessions.map((session, index) => { const info = resolveAgentInfo(session, profilesById); const isPrimary = session.id === primarySessionId; const isOpen = Boolean(api?.getPanel(`session:${session.id}`)); return ( handleClick(session.id, info.label, groupId)} className={`cursor-pointer text-xs gap-1.5 ${isOpen ? "opacity-50" : ""}`} data-testid={`reopen-session-${session.id}`} > #{index + 1} {info.agentName && ( )} {info.label} {isPrimary && } {session.state !== "RUNNING" && session.state !== "STARTING" && session.state !== "WAITING_FOR_INPUT" && ( {getSessionStateIcon(session.state, "h-3 w-3")} )} ); })} ); }