import React, { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle } from 'react'; import { useChat } from '../hooks/useChat'; import { ChatBody } from './ChatBody'; import { InteractivePromptOverlay } from './InteractivePromptOverlay'; import { getBrand } from '@/lib/adapter-brands'; import { extractTextFromBlocks } from '@/lib/content-utils'; import { api } from '@/lib/api'; // ===== Types ===== export interface ReviewEntry { reviewId: string; childSessionId: string; childAdapter: string; reviewTitle?: string; prompt?: string; permissionMode?: string; } interface ReviewPanelProps { reviews: ReviewEntry[]; onEnd: (reviewId: string) => void; onMinimize: () => void; cwd?: string; onSessionCreated?: (reviewId: string, childSessionId: string) => void; readOnly?: boolean; } export interface ReviewPanelHandle { sendToReview: (reviewId: string, text: string) => void; } // ===== ReviewTab (one per review, keeps useChat hook alive) ===== const ReviewTab = React.memo(function ReviewTab({ review, cwd, onSessionCreated, isActive, readOnly, sendRef }: { review: ReviewEntry; cwd?: string; onSessionCreated?: (reviewId: string, sid: string) => void; isActive: boolean; readOnly?: boolean; sendRef?: React.MutableRefObject void>>; }) { const { messages, streaming, pendingResponse, liveStatus, toolStatuses, sendMessage, abort, sessionId: chatSessionId, interactivePrompt, respondPrompt, } = useChat( review.childSessionId || undefined, cwd, review.childAdapter, review.prompt, review.permissionMode, ); // Notify parent when child session is created const sessionCreatedRef = useRef(false); useEffect(() => { if (chatSessionId && !review.childSessionId && onSessionCreated && !sessionCreatedRef.current) { sessionCreatedRef.current = true; onSessionCreated(review.reviewId, chatSessionId); } }, [chatSessionId, review.childSessionId, onSessionCreated, review.reviewId]); // Register sendMessage in parent's ref map for sendToReview useEffect(() => { if (sendRef && review.reviewId) { sendRef.current.set(review.reviewId, sendMessage); return () => { sendRef.current.delete(review.reviewId); }; } }, [sendRef, review.reviewId, sendMessage]); const brand = getBrand(review.childAdapter); // Send-back handler: extract text from message and send to parent via API const handleSendBack = useCallback(async (messageId: string) => { const msg = messages.find(m => m.id === messageId); if (!msg) return; const text = extractTextFromBlocks(msg.content); if (!review.reviewId) { console.warn('Send back unavailable: review not yet registered'); return; } try { await api.sendBackToParent(review.reviewId, text); } catch (err: any) { console.error('Send back failed:', err.message || err); } }, [messages, review.reviewId]); return (
{interactivePrompt && ( )}
); }); // ===== Main Panel ===== export const FloatingReviewPanel = forwardRef( function FloatingReviewPanel({ reviews, onEnd, onMinimize, cwd, onSessionCreated, readOnly }, ref) { const [activeTabIndex, setActiveTabIndex] = useState(Math.max(0, reviews.length - 1)); // Keep activeTabIndex in bounds useEffect(() => { if (activeTabIndex >= reviews.length) { setActiveTabIndex(Math.max(0, reviews.length - 1)); } }, [reviews.length, activeTabIndex]); // Auto-focus newest tab when a review is added const prevCountRef = useRef(reviews.length); useEffect(() => { if (reviews.length > prevCountRef.current) { setActiveTabIndex(reviews.length - 1); } prevCountRef.current = reviews.length; }, [reviews.length]); // Ref map: reviewId → sendMessage function (populated by each ReviewTab) const sendRefs = useRef void>>(new Map()); useImperativeHandle(ref, () => ({ sendToReview(reviewId: string, text: string) { const send = sendRefs.current.get(reviewId); if (send) send(text); const idx = reviews.findIndex(r => r.reviewId === reviewId); if (idx >= 0) setActiveTabIndex(idx); }, }), [reviews]); const activeReview = reviews[activeTabIndex] || reviews[0]; if (!activeReview) return null; const brand = getBrand(activeReview.childAdapter); return (
{/* Handle bar */}
{/* Tab bar (multiple reviews) or single-review header */} {reviews.length > 1 ? (
{reviews.map((r, i) => { const b = getBrand(r.childAdapter); const tabActive = i === activeTabIndex; return (
{!readOnly && ( )}
); })}
{!readOnly && ( )}
) : (
{brand.displayName} {activeReview.reviewTitle || 'Review Session'} {readOnly && (ended)}
)} {/* Tabs — ALL rendered to keep hooks alive, only active one visible via CSS */} {reviews.map((r, i) => ( ))}
); } );