"use client"; import { useEffect, useId, useRef, useState, type RefObject } from "react"; import { IconChevronDown, IconChevronUp, IconMessageQuestion } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { useTranslation } from "react-i18next"; import { ClarificationInputOverlay } from "./clarification-input-overlay"; import { useComposerDisclosureContext } from "./composer-disclosure"; import { ResizeHandle } from "./resize-handle"; import { useResizableClarificationOverlay } from "@/hooks/use-resizable-clarification-overlay"; import type { ClarificationRequestMetadata, Message } from "@/lib/types/http"; import type { ClarificationOutcome } from "@/hooks/domains/session/use-clarification-group"; type ClarificationPanelSectionProps = { pending: boolean; messages: readonly Message[] | null | undefined; onResolved: () => void; shortcutScopeRef: RefObject; /** * True when the session no longer has a live clarification waiter. * Detached requests remain answerable even if their row refresh lags. */ agentDisconnected?: boolean; /** * Caps the expanded overlay's height as a percentage of the viewport. * Drives both the rendered CSS max-height and the resize hook's drag clamp * - the two MUST agree, or the drag handle silently stops responding before * its own visible ceiling. */ maxHeightVh: number; // Additive: forwarded straight through to ClarificationInputOverlay. // Existing hosts (task chat, Quick Chat) leave this unset. onOutcome?: (outcome: ClarificationOutcome) => void; }; function pendingIdFromMessages(messages: readonly Message[] | null | undefined): string | null { const first = messages?.[0]; if (!first) return null; const metadata = first.metadata as ClarificationRequestMetadata | undefined; return metadata?.pending_id ?? null; } // Tracks collapse state per bundle: a bundle the user dismissed stays // collapsed while it's still pending, but a *new* bundle (different // pending_id) always opens expanded, even if the previous one never resolved. function useCollapsedForBundle(pendingId: string | null) { const [collapsed, setCollapsed] = useState(false); const lastPendingId = useRef(pendingId); useEffect(() => { if (pendingId !== lastPendingId.current) { lastPendingId.current = pendingId; setCollapsed(false); } }, [pendingId]); return [collapsed, setCollapsed] as const; } /** * Collapsible clarification section shared by the task chat panel and Quick * Chat. Escape (or the labelled toggle) collapses this to a persistent header * bar instead of answering or rejecting the bundle, so a blocked agent stays * visibly blocked and the question stays reachable until answered or skipped. */ export function ClarificationPanelSection({ pending, messages, onResolved, shortcutScopeRef, agentDisconnected = false, maxHeightVh, onOutcome, }: ClarificationPanelSectionProps) { const { t } = useTranslation(); const disclosure = useComposerDisclosureContext(); const pendingId = pendingIdFromMessages(messages); const [collapsed, setCollapsed] = useCollapsedForBundle(pendingId); const contentId = useId(); const { height, containerRef, resetHeight, resizeHandleProps } = useResizableClarificationOverlay( maxHeightVh / 100, ); // A newly opened clarification starts expanded and auto-sized. Collapsing // an active one leaves its form state and user-selected height intact. useEffect(() => { if (!pending) resetHeight(); }, [pending, resetHeight]); // A new bundle (different pending_id) replacing a still-pending one must // not inherit the previous bundle's dragged height. useEffect(() => { resetHeight(); }, [pendingId, resetHeight]); if (!pending) return null; const questionCount = messages?.length ?? 0; const actionLabel = collapsed ? t("chat:expandClarification") : t("chat:collapseClarification"); return ( setCollapsed(true)} onOutcome={onOutcome} onResolved={onResolved} onToggleCollapse={() => setCollapsed((current) => !current)} questionCount={questionCount} resizeHandleProps={resizeHandleProps} shortcutScopeRef={shortcutScopeRef} /> ); } type ClarificationPanelContentProps = { actionLabel: string; agentDisconnected: boolean; collapsed: boolean; containerRef: RefObject; contentId: string; disclosure: ReturnType; height: number | null; maxHeightVh: number; messages: readonly Message[] | null | undefined; onCollapse: () => void; onOutcome?: (outcome: ClarificationOutcome) => void; onResolved: () => void; onToggleCollapse: () => void; questionCount: number; resizeHandleProps: Parameters[0]; shortcutScopeRef: RefObject; }; function ClarificationPanelContent({ actionLabel, agentDisconnected, collapsed, containerRef, contentId, disclosure, height, maxHeightVh, messages, onCollapse, onOutcome, onResolved, onToggleCollapse, questionCount, resizeHandleProps, shortcutScopeRef, }: ClarificationPanelContentProps) { const { t } = useTranslation(); const compact = collapsed || questionCount === 0; return (
{!compact && }
{compact && (
{t("chat:clarificationNeeded")} {questionCount > 0 && ( {t("task:questionCount", { count: questionCount })} )}
)}
); }