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}
))}
);
}