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