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 ? (
) : (
)}
>
)}
);
}