import { useMemo, useRef } from "react"; import { formatReviewType } from "@/lib/format"; import type { FindingFixResult, FindingId, StoredFinding, } from "@/lib/review-workflow/findings/types"; import { storedFindingToFinding } from "@/lib/review-workflow/presentation"; import type { SessionState } from "@/lib/session-state"; import { resolveIssuesFoundDisplay } from "@/lib/tui/sessions/issues-found-display"; import { extractLatestReviewSummary, findLatestReviewerPhaseStart, } from "@/lib/tui/sessions/review-summary-parser"; import { formatSessionIdentityDisplay } from "@/lib/tui/sessions/session-display"; import { TUI_COLORS } from "@/lib/tui/shared/colors"; import { ProgressBar } from "@/lib/tui/shared/ProgressBar"; import { Spinner } from "@/lib/tui/shared/Spinner"; import type { AgentRole, Finding, FixEntry, ReviewOptions, ReviewSummary, SkippedEntry, } from "@/lib/types"; import { parseCodexReviewText } from "@/lib/types"; import { FindingFixResultList, FindingsList, FixList, SectionHeader, SelectableStoredFindingsList, SkippedList, StoredFindingsList, toSingleLine, } from "./session-detail-parts"; export interface SessionDetailViewProps { session: SessionState; fixes: FixEntry[]; skipped: SkippedEntry[]; findings: Finding[]; storedFindings: StoredFinding[]; selectedFindingIds: FindingId[]; fixResults: FindingFixResult[]; unresolvedSelectedFindings: StoredFinding[]; auditRegressionFindings: StoredFinding[]; latestReviewIteration: number | null; codexReviewText: string | null; tmuxOutput: string; maxIterations: number; currentAgent: AgentRole | null; reviewOptions: ReviewOptions | undefined; isStopping: boolean; activeSessionCount: number; focused?: boolean; } type BoxHeight = number | "auto" | `${number}%`; const METADATA_LABEL_WIDTH = 12; const METADATA_VALUE_INDENT = METADATA_LABEL_WIDTH + 1; function KeyValueRow({ label, children }: { label: string; children: React.ReactNode }) { return ( {label} {children} ); } function MetadataDivider() { return ( {"─".repeat(40)} ); } function getStatusDisplay( status: string, currentAgent: AgentRole | null, isPreparing = false, currentPhase?: string ): { text: string; color: string } { switch (status) { case "completed": return { text: "completed", color: TUI_COLORS.status.success }; case "failed": return { text: "failed", color: TUI_COLORS.status.error }; case "interrupted": return { text: "interrupted", color: TUI_COLORS.status.warning }; case "running": if (currentAgent) { return { text: `running ${currentAgent} agent`, color: TUI_COLORS.status.success }; } if (isPreparing) { return { text: "preparing session worktree", color: TUI_COLORS.status.pending }; } return { text: "running", color: TUI_COLORS.status.success }; case "pending": return { text: currentPhase && currentPhase !== "review" ? "starting fix" : "starting review", color: TUI_COLORS.status.pending, }; default: return { text: "unknown", color: TUI_COLORS.status.inactive }; } } function countCodexReviewLines(text: string): number { return text.split("\n").filter((line) => line.trim() !== "").length; } function CodexReviewDisplay({ text, height = 6, focused = false, }: { text: string; height?: BoxHeight; focused?: boolean; }) { const lines = text.split("\n").filter((line) => line.trim() !== ""); if (lines.length === 0) { return ( No review text ); } const content = lines.map((line, index) => ( {line} )); return ( {content} ); } export function SessionDetailView({ session, fixes, skipped, findings, storedFindings, selectedFindingIds, fixResults, unresolvedSelectedFindings, auditRegressionFindings, latestReviewIteration, codexReviewText, tmuxOutput, maxIterations, currentAgent, reviewOptions, isStopping, activeSessionCount, focused = false, }: SessionDetailViewProps) { const sessionIteration = session.iteration ?? 0; const parsedCodexSummary = useMemo(() => { if (!codexReviewText) return null; return parseCodexReviewText(codexReviewText); }, [codexReviewText]); const reviewerPhaseStart = useMemo(() => findLatestReviewerPhaseStart(tmuxOutput), [tmuxOutput]); const liveReviewSummary = useMemo(() => { if (!tmuxOutput.trim()) return null; const minIndex = reviewerPhaseStart >= 0 ? reviewerPhaseStart : 0; return extractLatestReviewSummary(tmuxOutput, minIndex); }, [tmuxOutput, reviewerPhaseStart]); const lastLiveReviewSummaryRef = useRef<{ iteration: number; summary: ReviewSummary } | null>( null ); if (liveReviewSummary) { lastLiveReviewSummaryRef.current = { iteration: sessionIteration, summary: liveReviewSummary, }; } else if ( lastLiveReviewSummaryRef.current && lastLiveReviewSummaryRef.current.iteration !== sessionIteration ) { lastLiveReviewSummaryRef.current = null; } const iteration = sessionIteration; const statusDisplay = getStatusDisplay( session.state ?? "unknown", currentAgent, session.state === "running" && currentAgent === null && session.iteration === undefined && session.currentPhase === undefined, session.currentPhase ); const cachedLiveReviewSummary = lastLiveReviewSummaryRef.current?.iteration === iteration ? lastLiveReviewSummaryRef.current.summary : null; const { findings: displayFindings, codexText: displayCodexText } = resolveIssuesFoundDisplay({ sessionStatus: session.state, sessionIteration: iteration, latestReviewIteration, persistedFindings: findings, persistedCodexText: codexReviewText, parsedCodexSummary, liveReviewSummary, cachedLiveReviewSummary, sessionStateReviewSummary: session.reviewSummary ?? null, }); const showingCodex = displayCodexText !== null && displayFindings.length === 0; const verifyCount = showingCodex && displayCodexText ? countCodexReviewLines(displayCodexText) : displayFindings.length; const appliedCount = fixes.length; const skippedCount = skipped.length; const batchFirstMode = session.currentPhase !== undefined || session.sessionStatus !== undefined || session.reviewOutcome !== undefined || storedFindings.length > 0 || (session.accumulatedFindings?.length ?? 0) > 0 || selectedFindingIds.length > 0 || fixResults.length > 0 || unresolvedSelectedFindings.length > 0 || auditRegressionFindings.length > 0; const inventoryFindings = session.accumulatedFindings ?? storedFindings; const workflowFindingsById = new Map( [...inventoryFindings, ...auditRegressionFindings].map((finding) => [finding.id, finding]) ); const workflowSelectedIds = session.selectedFindingIds && session.selectedFindingIds.length > 0 ? session.selectedFindingIds : selectedFindingIds; const workflowUnresolvedFindings = unresolvedSelectedFindings.length > 0 ? unresolvedSelectedFindings : fixResults .filter((result) => result.status === "unresolved") .map((result) => workflowFindingsById.get(result.findingId)) .filter((finding): finding is StoredFinding => finding !== undefined); const workflowRegressionFindings = auditRegressionFindings; const batchDisplayFindings = inventoryFindings.length > 0 ? inventoryFindings.map(storedFindingToFinding) : displayFindings; const sessionIdentity = formatSessionIdentityDisplay(session, activeSessionCount); const showIterationProgress = currentAgent !== "fixer"; return ( {isStopping ? ( <> Stopping review... ) : ( <> {(session.state === "running" || session.state === "pending") && ( )} {statusDisplay.text} )} {toSingleLine(formatReviewType(reviewOptions))} {sessionIdentity.primary} {sessionIdentity.details.map((detail) => ( {detail} ))} {showIterationProgress && ( <> )} {batchFirstMode ? ( <> 0 ? ( · {workflowSelectedIds.length} selected ) : undefined } /> {inventoryFindings.length > 0 ? ( ) : showingCodex ? ( ) : ( )} {fixResults.length > 0 && ( )} {(workflowUnresolvedFindings.length > 0 || workflowRegressionFindings.length > 0) && ( {workflowUnresolvedFindings.length > 0 && ( Unresolved selected findings )} {workflowRegressionFindings.length > 0 && ( Regression findings )} )} ) : ( <> · codex : undefined} /> {showingCodex ? ( ) : ( )} )} ); }