import { useMemo, useState, type JSX } from "react"; import { translate } from "../../../../../i18n/index"; import { workspaceFilePathBasename } from "../../../../../actions/workspaceFilePathCandidate"; import { AgentToolScrollArea } from "../AgentToolScrollArea"; import { AgentPathTailLabel } from "../AgentPathTailLabel"; import { parseAgentUnifiedDiffLines, parseAgentUnifiedDiffStats } from "./agentUnifiedDiff"; const MAX_VISIBLE_LINES = 120; export function AgentUnifiedPatchViewer({ path, diffText, showHeader = true, collapsible = false, compact = false, flat = false }: { path?: string | null; diffText: string; showHeader?: boolean; collapsible?: boolean; compact?: boolean; flat?: boolean; }): JSX.Element | null { "use memo"; const [expanded, setExpanded] = useState(false); const normalized = diffText.trimEnd(); const lines = useMemo( () => (normalized ? visiblePatchLines(normalized, compact) : []), [compact, normalized] ); const diffLines = useMemo( () => (flat && normalized ? parseAgentUnifiedDiffLines(normalized) : []), [flat, normalized] ); const stats = useMemo( () => normalized ? parseAgentUnifiedDiffStats(normalized) : { added: 0, removed: 0 }, [normalized] ); const fileLabel = fileNameFromPath(path) ?? path ?? "Patch"; const truncated = collapsible && !expanded && lines.length > MAX_VISIBLE_LINES; const visibleLines = useMemo( () => (truncated ? lines.slice(0, MAX_VISIBLE_LINES) : lines).map( (line, lineIndex) => ({ key: `${lineIndex + 1}:${line}`, line }) ), [lines, truncated] ); const visibleDiffLines = useMemo( () => (collapsible && !expanded && diffLines.length > MAX_VISIBLE_LINES ? diffLines.slice(0, MAX_VISIBLE_LINES) : diffLines ).map((line) => ({ key: `${line.kind}:${line.oldLineNumber ?? "x"}:${line.newLineNumber ?? "x"}:${line.text}`, line })), [collapsible, diffLines, expanded] ); if (!normalized) { return null; } const disclosureButton = collapsible && lines.length > MAX_VISIBLE_LINES ? ( ) : null; return (
{flat ? ( <> {showHeader ? (
{fileLabel} +{stats.added} -{stats.removed}
) : null} {visibleDiffLines.map(({ key, line }) => (
{line.newLineNumber ?? line.oldLineNumber ?? ""}
                  {line.text || " "}
                
))} {disclosureButton}
) : ( <> {showHeader ? (
) : null} {visibleLines.map(({ key, line }) => (
{line || " "}
))}
{disclosureButton} )}
); } function fileNameFromPath(path: string | null | undefined): string | null { if (!path) { return null; } const normalized = path.trim(); if (!normalized) { return null; } return workspaceFilePathBasename(normalized) || normalized; } function flatLineClassName(kind: "add" | "remove" | "context"): string { if (kind === "add") { return "grid grid-cols-[48px_minmax(0,1fr)] border-l-[2px] border-l-[var(--state-success)] bg-[color:color-mix(in_srgb,var(--state-success)_10%,transparent)] font-[var(--tsh-font-mono)] text-[11px] leading-6"; } if (kind === "remove") { return "grid grid-cols-[48px_minmax(0,1fr)] border-l-[2px] border-l-[var(--state-danger)] bg-[color:color-mix(in_srgb,var(--state-danger)_10%,transparent)] font-[var(--tsh-font-mono)] text-[11px] leading-6"; } return "agent-tool-diff__line grid grid-cols-[48px_minmax(0,1fr)] font-[var(--tsh-font-mono)] text-[11px] leading-6"; } function visiblePatchLines(value: string, compact: boolean): string[] { const lines = value.split("\n"); if (!compact) { return lines; } const firstHunkIndex = lines.findIndex((line) => line.startsWith("@@")); if (firstHunkIndex <= 0) { return lines; } return lines.slice(firstHunkIndex); } function lineClassName(line: string): string { if (line.startsWith("@@")) { return "border-y border-[var(--line-2)] bg-[var(--transparency-block)] px-2 py-1 text-[var(--text-secondary)]"; } if ( line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") ) { return "px-2 py-0.5 text-[var(--text-tertiary)]"; } if (line.startsWith("+")) { return "bg-[color:color-mix(in_srgb,var(--state-success)_10%,transparent)] px-2 py-0.5 text-[var(--state-success)]"; } if (line.startsWith("-")) { return "bg-[color:color-mix(in_srgb,var(--state-danger)_10%,transparent)] px-2 py-0.5 text-[var(--state-danger)]"; } return "px-2 py-0.5 text-[var(--text-primary)]"; }