"use client"; import { useMemo, useState } from "react"; import { IconColumns } from "@tabler/icons-react"; import type { ActiveThread } from "@/lib/threads/active-threads"; import { useTranslation } from "react-i18next"; import { ThreadColumn } from "./thread-column"; import { useThreadColumnActivation } from "./use-thread-column-activation"; type ThreadsBoardProps = { threads: ActiveThread[]; isLoading?: boolean; /** Column a deep link asked for; scrolled into view and ringed on arrival. */ focusedTaskId?: string | null; /** Session a task-detail link asked the target column to select. */ focusedSessionId?: string | null; /** Removes a target session query after the target column proves it invalid. */ onInvalidRequestedSession?: (taskId: string, sessionId: string) => void; onOpenTask: (taskId: string) => void; }; function ThreadsPlaceholder({ testId, children }: { testId: string; children: React.ReactNode }) { return (
{children}
); } function ThreadsEmptyState() { const { t } = useTranslation(); return ( ); } function ThreadsLoadingState() { const { t } = useTranslation(); return (

{t("threads:loading")}

); } /** * The deep-link mark answers "where is the column I asked for", so it retires * the moment the reader starts using the deck rather than sitting on a column * they have since moved away from. A later deep link earns a fresh mark, which * is why dismissal is keyed to the requested id rather than latched forever. * * Uses the store-previous-props pattern instead of an effect so the mark never * paints for a frame after a new request has already been dismissed. */ function useRetiringFocusMark(focusedTaskId: string | null) { const [retired, setRetired] = useState(false); const [requested, setRequested] = useState(focusedTaskId); if (requested !== focusedTaskId) { setRequested(focusedTaskId); setRetired(false); } return { markedTaskId: retired ? null : focusedTaskId, retire: () => setRetired(true), }; } /** * The deck: every live agent conversation as its own column, scrolled * horizontally. Columns keep the order the selector gave them, so a thread the * reader is following does not jump while they read it. */ export function ThreadsBoard({ threads, isLoading = false, focusedTaskId = null, focusedSessionId = null, onInvalidRequestedSession, onOpenTask, }: ThreadsBoardProps) { const { markedTaskId, retire } = useRetiringFocusMark(focusedTaskId); const orderedIds = useMemo(() => threads.map((thread) => thread.taskId), [threads]); const { boardRef, registerColumn, preloadTaskIds, detailTaskIds } = useThreadColumnActivation( orderedIds, focusedTaskId, ); if (threads.length === 0) { return isLoading ? : ; } return (
{threads.map((thread) => ( ))}
); }