import type { ScrollBoxRenderable } from "@opentui/core"; import { type ReactNode, useRef } from "react"; import { formatDuration } from "@/lib/format"; import { getProjectNameFromLogPath } from "@/lib/logger"; import { deriveWorkflowPresentationData } from "@/lib/review-workflow/presentation"; import { FindingFixResultList, FindingsList, SectionHeader, SkippedList, StoredFindingsList, toSingleLine, } from "@/lib/tui/sessions/detail/session-detail-parts"; import { buildScrollBarRows, useScrollMetrics, } from "@/lib/tui/sessions/detail/session-detail-scroll"; import { PriorityText } from "@/lib/tui/sessions/priority-text"; import { formatHandoffSummary, formatLastRunIssueSummary, formatPriorityBreakdown, formatProjectNameForDisplay, } from "@/lib/tui/sessions/session-display"; import { TUI_COLORS } from "@/lib/tui/shared/colors"; import type { Finding, FixEntry, IterationEntry, SessionEndEntry, SessionStats, SkippedEntry, SystemEntry, } from "@/lib/types"; const META_LABEL_WIDTH = 16; const LOCATION_MAX_LENGTH = 92; function statusColor(status: string): string { switch (status) { case "completed": return TUI_COLORS.status.success; case "running": return TUI_COLORS.status.pending; case "failed": return TUI_COLORS.status.error; case "interrupted": return TUI_COLORS.status.warning; default: return TUI_COLORS.status.inactive; } } function truncateMiddle(value: string, maxLength: number): string { if (value.length <= maxLength || maxLength < 9) { return value; } const visible = maxLength - 3; const leftLength = Math.ceil(visible / 2); const rightLength = Math.floor(visible / 2); return `${value.slice(0, leftLength)}...${value.slice(-rightLength)}`; } function formatSessionTimestamp(timestamp: number): string { const date = new Date(timestamp); const year = String(date.getFullYear()); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}`; } function IterationHeading({ iteration, duration }: { iteration: number; duration?: number }) { return ( Iteration {iteration} {duration !== undefined && {formatDuration(duration)}} ); } function formatLocationLine(fix: FixEntry): string | null { const baseFile = fix.file?.trim() || fix.code_location?.absolute_file_path?.trim() || ""; if (!baseFile) { return null; } if (!fix.code_location) { return truncateMiddle(baseFile, LOCATION_MAX_LENGTH); } const range = fix.code_location.line_range; return truncateMiddle(`${baseFile}:${range.start}-${range.end}`, LOCATION_MAX_LENGTH); } function MetadataRow({ label, children }: { label: string; children: ReactNode }) { return ( {label} {children} ); } function HeaderSectionTitle({ title }: { title: string }) { return ( {title} ); } function PrioritySummaryRow({ priorityCounts, }: { priorityCounts: SessionStats["priorityCounts"]; }) { return ( {formatPriorityBreakdown(priorityCounts).map((item) => ( {item.count} ))} ); } function LabeledFixDetail({ label, value }: { label: string; value: string }) { const text = value.trim().length > 0 ? toSingleLine(value) : "None provided"; const textColor = value.trim().length > 0 ? TUI_COLORS.text.secondary : TUI_COLORS.text.dim; return ( {label}: {text} ); } function DetailedFixList({ fixes }: { fixes: FixEntry[] }) { if (fixes.length === 0) { return ( None yet ); } return ( {fixes.map((fix, index) => { const locationLine = formatLocationLine(fix); return ( {toSingleLine(fix.title)} {locationLine && ( {toSingleLine(locationLine)} )} ); })} ); } function IterationSection({ entry }: { entry: IterationEntry }) { const findings: Finding[] = entry.review?.findings ?? []; const displayFindings: Finding[] = findings.map((finding) => ({ ...finding, code_location: { ...finding.code_location, absolute_file_path: truncateMiddle( finding.code_location.absolute_file_path, LOCATION_MAX_LENGTH ), }, })); const fixes: FixEntry[] = entry.fixes?.fixes ?? []; const skipped: SkippedEntry[] = entry.fixes?.skipped ?? []; const hasContent = findings.length > 0 || fixes.length > 0 || skipped.length > 0; return ( {entry.fixes && ( Decision: {entry.fixes.decision} )} {!hasContent && !entry.error && ( No issues found )} {findings.length > 0 && ( )} {fixes.length > 0 && ( )} {skipped.length > 0 && ( )} {entry.error && ( Error: {entry.error.message} )} ); } function formatBatchFirstResult(stats: SessionStats): string { const findingsText = `${stats.totalFindings ?? 0} issues found`; const selectionText = stats.totalSelectedFindings !== undefined ? ` · ${stats.totalSelectedFindings} selected` : ""; const remediationText = stats.totalResolvedSelectedFindings !== undefined || stats.totalUnresolvedSelectedFindings !== undefined ? ` · ${stats.totalResolvedSelectedFindings ?? 0} resolved · ${stats.totalUnresolvedSelectedFindings ?? 0} unresolved` : ""; return `${findingsText}${selectionText}${remediationText}`; } function WorkflowSection({ title, children }: { title: string; children: ReactNode }) { return ( {title} {children} ); } function SessionEndSection({ entry }: { entry: SessionEndEntry }) { return ( Result {entry.status} {entry.reason} ); } export function SessionDetailPane({ stats, focused, height, }: { stats: SessionStats; focused?: boolean; height?: number; }) { const scrollboxRef = useRef(null); const scrollMetrics = useScrollMetrics(scrollboxRef, focused); const workflow = deriveWorkflowPresentationData(stats.entries); const hasBatchFirstSummary = workflow.hasBatchFirstLifecycle || stats.reviewOutcome === "findings-pending" || stats.totalFindings !== undefined; const issueSummary = hasBatchFirstSummary ? formatBatchFirstResult(stats) : formatLastRunIssueSummary(stats.totalFixes, stats.totalSkipped, stats.iterations); const handoffSummary = formatHandoffSummary(stats.handoffStatus, stats.commitSha); const outcomeSummary = [stats.reviewOutcome, handoffSummary].filter(Boolean).join(" · "); const projectName = formatProjectNameForDisplay(getProjectNameFromLogPath(stats.sessionPath)); const sessionTimestamp = formatSessionTimestamp(stats.timestamp); const systemEntry = stats.entries.find((e) => e.type === "system") as SystemEntry | undefined; const iterationEntries = stats.entries.filter((e) => e.type === "iteration") as IterationEntry[]; const sessionEndEntry = stats.entries.find((e) => e.type === "session_end") as | SessionEndEntry | undefined; const reasoningLabel = (level: string | undefined) => (level ? `[${level}]` : ""); const scrollbarRows = buildScrollBarRows(scrollMetrics); return ( {projectName} {sessionTimestamp} {stats.status} {stats.totalDuration !== undefined && ( · {formatDuration(stats.totalDuration)} )} {issueSummary} {outcomeSummary && ( {outcomeSummary} )} {(stats.phase || stats.sessionStatus) && ( {[stats.phase, stats.sessionStatus].filter(Boolean).join(" · ")} )} {stats.commitSha && ( {stats.commitSha} )} {stats.gitBranch && ( {stats.gitBranch} {stats.worktreeBranch ? ` (worktree: ${stats.worktreeBranch})` : ""} )} {stats.reviewerDisplayName} ({stats.reviewerModelDisplayName}){" "} {reasoningLabel(stats.reviewerReasoning)} {stats.fixerDisplayName} ({stats.fixerModelDisplayName}){" "} {reasoningLabel(stats.fixerReasoning)} {systemEntry && ( {systemEntry.maxIterations} )} {workflow.hasBatchFirstLifecycle ? ( <> {workflow.reviewEntries.map((entry) => ( {entry.findings.length} issues found ))} {workflow.selectionEntry && ( {workflow.selectionEntry.selectedFindingIds.length} selected via{" "} {workflow.selectionEntry.selectionMode} {workflow.selectedFindings.length > 0 && ( )} )} {workflow.batchFixEntry && ( )} {(workflow.unresolvedSelectedFindings.length > 0 || workflow.regressionFindings.length > 0) && ( {workflow.unresolvedSelectedFindings.length > 0 && ( Unresolved selected findings )} {workflow.regressionFindings.length > 0 && ( Regression findings )} )} ) : ( iterationEntries.map((entry) => ( )) )} {/* Session end */} {sessionEndEntry && } {scrollbarRows.map((row) => ( {row.char} ))} ); }