fix(desktop): surface diff type badge in card title when file header is collapsed

Instead of forcing the per-file header open just to show the New
file/Deleted badge (previous fix), keep the header collapsed when it
would only repeat the card title, and show the badge in the diff card's
title bar instead — both in the inline DiffMessage card and the expanded
dialog.

DIFF_TYPE_LABELS and the header-visibility check move into parseDiff.ts
as shared helpers, and a new getDiffTitleBadge returns the badge label
only for single-file diffs whose collapsed header has a notable change
type — so the badge never renders in both places.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-07-10 17:23:23 +10:00
co-authored by Claude Fable 5
parent e3c9dfcfee
commit 0683e35d86
4 changed files with 71 additions and 14 deletions
@@ -33,6 +33,14 @@ export function parseUnifiedDiff(content: string): ParsedDiffResult {
}
}
export const DIFF_TYPE_LABELS: Record<DiffType, string> = {
add: "New file",
copy: "Copied",
delete: "Deleted",
modify: "Modified",
rename: "Renamed",
};
export function getDiffFileLabel(
file: FileData,
fallbackFilePath?: string,
@@ -47,6 +55,39 @@ export function getDiffFileLabel(
return newPath || oldPath || fallbackFilePath || "diff";
}
export function shouldShowDiffFileHeader(
label: string,
fileCount: number,
fallbackFilePath?: string,
): boolean {
return fileCount > 1 || !fallbackFilePath || label !== fallbackFilePath;
}
/**
* Badge for the diff card's title bar. Set only when the diff is a single
* file whose per-file header is collapsed (its label just repeats the card
* title) and whose change type is notable — so "New file"/"Deleted" isn't
* lost with the header.
*/
export function getDiffTitleBadge(
content: string,
fallbackFilePath?: string,
): string | undefined {
const { files } = parseUnifiedDiff(content);
if (files.length !== 1) {
return undefined;
}
const file = files[0];
const label = getDiffFileLabel(file, fallbackFilePath);
if (shouldShowDiffFileHeader(label, files.length, fallbackFilePath)) {
return undefined;
}
const diffType = normalizeDiffType(file.type);
return diffType === "modify" ? undefined : DIFF_TYPE_LABELS[diffType];
}
export function countDiffFileChanges(file: FileData) {
let additions = 0;
let deletions = 0;
@@ -1,6 +1,7 @@
import * as React from "react";
import { FileDiff, Maximize2 } from "lucide-react";
import { getDiffTitleBadge } from "@/features/messages/lib/parseDiff";
import { isSafeUrl } from "@/shared/lib/url";
import { Button } from "@/shared/ui/button";
import { useSmoothCorners } from "@/shared/ui/smoothCorners";
@@ -39,6 +40,11 @@ export default function DiffMessage({
const safeRepoUrl = isSafeUrl(repoUrl) ? repoUrl : undefined;
const titleBadge = React.useMemo(
() => getDiffTitleBadge(content, filePath),
[content, filePath],
);
const commitUrl =
safeRepoUrl && commitSha ? `${safeRepoUrl}/commit/${commitSha}` : undefined;
@@ -54,6 +60,11 @@ export default function DiffMessage({
<span className="flex-1 truncate font-mono text-xs text-foreground/80">
{filePath ?? "diff"}
</span>
{titleBadge && (
<span className="shrink-0 rounded-md border border-border/60 px-1.5 py-0.5 text-2xs uppercase tracking-[0.14em] text-muted-foreground">
{titleBadge}
</span>
)}
{shortSha && (
<span className="text-xs text-muted-foreground font-mono">
{commitUrl ? (
@@ -1,6 +1,7 @@
import { Rows3, SplitSquareVertical } from "lucide-react";
import { useState } from "react";
import { useMemo, useState } from "react";
import { getDiffTitleBadge } from "@/features/messages/lib/parseDiff";
import { DiffViewer } from "@/features/messages/ui/DiffViewer";
import { Button } from "@/shared/ui/button";
import {
@@ -23,6 +24,11 @@ export default function DiffMessageExpanded({
}: DiffMessageExpandedProps) {
const [viewType, setViewType] = useState<"split" | "unified">("unified");
const titleBadge = useMemo(
() => getDiffTitleBadge(content, filePath),
[content, filePath],
);
return (
<Dialog
onOpenChange={(open) => {
@@ -36,6 +42,11 @@ export default function DiffMessageExpanded({
<DialogTitle className="min-w-0 flex-1 truncate font-mono text-sm font-medium">
{filePath ?? "Diff Viewer"}
</DialogTitle>
{titleBadge && (
<span className="shrink-0 rounded-md border border-border/60 px-1.5 py-0.5 text-2xs uppercase tracking-[0.14em] text-muted-foreground">
{titleBadge}
</span>
)}
<div className="flex items-center gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
<Button
className="h-7 px-2"
@@ -4,9 +4,11 @@ import { useMemo } from "react";
import {
countDiffFileChanges,
DIFF_TYPE_LABELS,
getDiffFileLabel,
normalizeDiffType,
parseUnifiedDiff,
shouldShowDiffFileHeader,
} from "@/features/messages/lib/parseDiff";
import { cn } from "@/shared/lib/cn";
import "./DiffViewer.css";
@@ -18,14 +20,6 @@ type DiffViewerProps = {
className?: string;
};
const DIFF_TYPE_LABELS = {
add: "New file",
copy: "Copied",
delete: "Deleted",
modify: "Modified",
rename: "Renamed",
} as const;
function FileChangeBadge({
tone,
value,
@@ -81,11 +75,11 @@ export function DiffViewer({
const label = getDiffFileLabel(file, fallbackFilePath);
const { additions, deletions } = countDiffFileChanges(file);
const diffType = normalizeDiffType(file.type);
const showFileHeader =
files.length > 1 ||
!fallbackFilePath ||
label !== fallbackFilePath ||
diffType !== "modify";
const showFileHeader = shouldShowDiffFileHeader(
label,
files.length,
fallbackFilePath,
);
const fileKey = [
file.oldPath || "",
file.newPath || "",