"use client"; import { useCallback, useMemo } from "react"; import { Button } from "@kandev/ui/button"; import { AgentLogo } from "@/components/agent-logo"; import { GridSpinner } from "@/components/grid-spinner"; import { PanelLoadingState } from "@/components/panel-loading-state"; import { SessionTabs, type SessionTab } from "@/components/session-tabs"; import { useAppStore } from "@/components/state-provider"; import { useToast } from "@/components/toast-provider"; import { useSessionResumption } from "@/hooks/domains/session/use-session-resumption"; import { useTaskSessions } from "@/hooks/use-task-sessions"; import type { UseEnsureTaskSessionResult } from "@/hooks/domains/session/use-ensure-task-session"; import type { AgentProfileOption } from "@/lib/state/slices"; import type { TaskSession } from "@/lib/types/http"; import { getWebSocketClient } from "@/lib/ws/connection"; import { PassthroughToolbar } from "./passthrough-toolbar"; import { TaskChatPanel } from "./task-chat-panel"; import { buildAgentLabelsById, isSessionActive, pickActiveSessionId, resolveAgentLabelFor, sortSessions, } from "./session-sort"; const LABEL_SEPARATOR = " \u2022 "; type PreviewSessionTabsProps = { taskId: string; sessionId: string | null; ensureSession?: UseEnsureTaskSessionResult; onSessionChange?: (sessionId: string | null) => void; }; /** * Read-only session tabs for the kanban preview panel. * * Tabs only switch between existing sessions — creating or deleting sessions * is deliberately restricted to the full-page task view. */ export function PreviewSessionTabs({ taskId, sessionId, ensureSession, onSessionChange, }: PreviewSessionTabsProps) { const { sessions, isLoaded } = useTaskSessions(taskId); const agentProfiles = useAppStore((state) => state.agentProfiles.items); const sortedSessions = useMemo(() => sortSessions(sessions), [sessions]); const agentLabelsById = useMemo(() => buildAgentLabelsById(agentProfiles), [agentProfiles]); const profilesById = useMemo( () => Object.fromEntries(agentProfiles.map((p) => [p.id, p])), [agentProfiles], ); const activeSessionId = useMemo( () => pickActiveSessionId(sortedSessions, sessionId), [sortedSessions, sessionId], ); const activeSession = useMemo( () => sortedSessions.find((s) => s.id === activeSessionId) ?? null, [sortedSessions, activeSessionId], ); // Mirrors the full-page task view: ensure the backend execution for the // active session is ready (resumes / restores workspace after a kandev // restart where the session row is persisted but agentctl isn't alive). useSessionResumption(taskId, activeSessionId); const tabs = useMemo( () => sortedSessions.map((session) => { const profile = session.agent_profile_id ? profilesById[session.agent_profile_id] : null; return { id: session.id, label: resolveProfileSubLabel(session, profile, agentLabelsById), icon: isSessionActive(session.state) ? ( ) : ( ), testId: `preview-session-tab-${session.id}`, className: "bg-muted/50 data-[state=active]:bg-muted", }; }), [sortedSessions, agentLabelsById, profilesById], ); if (!isLoaded && sortedSessions.length === 0) { return ; } if (sortedSessions.length === 0) { if (ensureSession?.status === "preparing") { return ; } if (ensureSession?.status === "error") { return ; } return ; } return (
onSessionChange?.(id)} listClassName="bg-transparent p-0 !h-7 gap-1 overflow-x-auto overflow-y-hidden min-w-0 shrink [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]" />
{activeSession && ( )}
); } function resolveProfileSubLabel( session: TaskSession, profile: AgentProfileOption | null | undefined, agentLabelsById: Record, ): string { const fullLabel = profile?.label ?? resolveAgentLabelFor(session, agentLabelsById); const parts = fullLabel.split(LABEL_SEPARATOR); return parts[1] ?? parts[0] ?? fullLabel; } function SessionAgentLogo({ profile }: { profile: AgentProfileOption | null | undefined }) { if (!profile?.agent_name) { // Keep tabs visually aligned when the agent profile is missing/unknown. return (