/** * PlanDiffViewer — Orchestrator for plan diff views * * Replaces the Viewer component when plan diff mode is active. * Renders the document card with the same styling, but shows * diff content instead of the annotatable plan. */ import React, { useState } from "react"; import type { PlanDiffBlock, PlanDiffStats } from "../../utils/planDiffEngine"; import type { Annotation, EditorMode } from "../../types"; import { PlanDiffModeSwitcher, type PlanDiffMode, } from "./PlanDiffModeSwitcher"; import { PlanCleanDiffView } from "./PlanCleanDiffView"; import { PlanRawDiffView } from "./PlanRawDiffView"; import { PlanDiffBadge } from "./PlanDiffBadge"; import { VSCodeIcon } from "./VSCodeIcon"; import { noteServerSessionMismatch, withServerSession } from "../../utils/serverSession"; interface PlanDiffViewerProps { diffBlocks: PlanDiffBlock[]; diffStats: PlanDiffStats; diffMode: PlanDiffMode; onDiffModeChange: (mode: PlanDiffMode) => void; onPlanDiffToggle: () => void; repoInfo?: { display: string; branch?: string } | null; baseVersionLabel?: string; baseVersion?: number; maxWidth?: number; // Annotation props annotations?: Annotation[]; onAddAnnotation?: (ann: Annotation) => void; onSelectAnnotation?: (id: string | null) => void; selectedAnnotationId?: string | null; mode?: EditorMode; onOpenVscodeDiff?: (baseVersion: number) => Promise<{ ok?: boolean; error?: string }>; } const defaultOpenVscodeDiff = async ( baseVersion: number ): Promise<{ ok?: boolean; error?: string }> => { try { const res = await fetch("/api/plan/vscode-diff", { method: "POST", headers: { "Content-Type": "application/json" }, // The stale-tab guard (a no-op for a host that never adopted a session). body: JSON.stringify(withServerSession({ baseVersion })), }); if (await noteServerSessionMismatch(res)) { return { error: "This review was replaced. Reload the page." }; } const data = (await res.json()) as { ok?: boolean; error?: string }; if (!res.ok || data.error) { return { error: data.error || "Failed to open VS Code diff" }; } return { ok: true }; } catch { return { error: "Failed to connect to server" }; } }; export const PlanDiffViewer: React.FC = ({ diffBlocks, diffStats, diffMode, onDiffModeChange, onPlanDiffToggle, repoInfo, baseVersionLabel, baseVersion, maxWidth, annotations, onAddAnnotation, onSelectAnnotation, selectedAnnotationId, mode, onOpenVscodeDiff, }) => { const [vscodeDiffLoading, setVscodeDiffLoading] = useState(false); const [vscodeDiffError, setVscodeDiffError] = useState(null); const canOpenVscodeDiff = baseVersion != null; const handleOpenVscodeDiff = async () => { if (!canOpenVscodeDiff || baseVersion == null) return; setVscodeDiffLoading(true); setVscodeDiffError(null); try { const result = await (onOpenVscodeDiff ?? defaultOpenVscodeDiff)(baseVersion); if (result.error) { setVscodeDiffError(result.error); } } catch { // A host-supplied opener that throws (instead of returning { error }) must // not wedge the button in a permanent loading state. setVscodeDiffError("Failed to open VS Code diff"); } finally { setVscodeDiffLoading(false); } }; return (
{/* Top-left: repo info + diff badge — matches Viewer layout (flex-col) so badge doesn't jump position */}
{repoInfo && (
{repoInfo.display} {repoInfo.branch && ( {repoInfo.branch} )}
)}
{/* Top-right: back to plan view button */}
{/* Diff mode switcher + version label + VS Code button */}
{baseVersionLabel && ( vs {baseVersionLabel} )} {canOpenVscodeDiff && ( )}
{/* VS Code diff error message */} {vscodeDiffError && (
{vscodeDiffError}
)} {/* Diff content */} {diffMode === "raw" ? ( ) : ( )}
); };