Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions desktop/src/features/messages/lib/parseDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,59 @@ 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,
): string {
if (file.oldPath && file.newPath && file.oldPath !== file.newPath) {
return `${file.oldPath} -> ${file.newPath}`;
const oldPath = file.oldPath === "/dev/null" ? undefined : file.oldPath;
const newPath = file.newPath === "/dev/null" ? undefined : file.newPath;

if (oldPath && newPath && oldPath !== newPath) {
return `${oldPath} -> ${newPath}`;
}

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;
}

return file.newPath || file.oldPath || fallbackFilePath || "diff";
const diffType = normalizeDiffType(file.type);
return diffType === "modify" ? undefined : DIFF_TYPE_LABELS[diffType];
}

export function countDiffFileChanges(file: FileData) {
Expand Down
93 changes: 53 additions & 40 deletions desktop/src/features/messages/ui/DiffMessage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -51,54 +57,61 @@ export default function DiffMessage({
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/50 bg-muted/40">
<FileDiff className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate font-mono text-xs text-foreground/80">
<span className="min-w-0 truncate font-mono text-xs text-foreground/80">
{filePath ?? "diff"}
</span>
{shortSha && (
<span className="text-xs text-muted-foreground font-mono">
{commitUrl ? (
{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="ml-auto flex items-center gap-2">
{shortSha && (
<span className="text-xs text-muted-foreground font-mono">
{commitUrl ? (
<a
className="hover:underline"
href={commitUrl}
rel="noreferrer noopener"
target="_blank"
>
{shortSha}
</a>
) : (
shortSha
)}
</span>
)}
{safeRepoUrl && !commitUrl && (
<span className="text-xs text-muted-foreground">
<a
className="hover:underline"
href={commitUrl}
href={safeRepoUrl}
rel="noreferrer noopener"
target="_blank"
>
{shortSha}
{getHostname(safeRepoUrl)}
</a>
) : (
shortSha
)}
</span>
)}
{safeRepoUrl && !commitUrl && (
<span className="text-xs text-muted-foreground">
<a
className="hover:underline"
href={safeRepoUrl}
rel="noreferrer noopener"
target="_blank"
>
{getHostname(safeRepoUrl)}
</a>
</span>
)}
{onExpand && (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label="Expand diff"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={onExpand}
size="sm"
type="button"
variant="ghost"
>
<Maximize2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Expand diff</TooltipContent>
</Tooltip>
)}
</span>
)}
{onExpand && (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label="Expand diff"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
onClick={onExpand}
size="sm"
type="button"
variant="ghost"
>
<Maximize2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Expand diff</TooltipContent>
</Tooltip>
)}
</div>
</div>

{description && (
Expand Down
17 changes: 14 additions & 3 deletions desktop/src/features/messages/ui/DiffMessageExpanded.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) => {
Expand All @@ -33,10 +39,15 @@ export default function DiffMessageExpanded({
<DialogContent className="max-w-5xl w-full h-[80vh] flex flex-col p-0 gap-0">
<DialogHeader className="shrink-0 border-b border-border/50 px-4 py-3 pr-14">
<div className="flex flex-wrap items-center gap-2">
<DialogTitle className="min-w-0 flex-1 truncate font-mono text-sm font-medium">
<DialogTitle className="min-w-0 truncate font-mono text-sm font-medium">
{filePath ?? "Diff Viewer"}
</DialogTitle>
<div className="flex items-center gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
{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="ml-auto flex items-center gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
<Button
className="h-7 px-2"
onClick={() => {
Expand Down
17 changes: 7 additions & 10 deletions desktop/src/features/messages/ui/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -79,10 +73,13 @@ export function DiffViewer({
<div className="space-y-3">
{files.map((file) => {
const label = getDiffFileLabel(file, fallbackFilePath);
const showFileHeader =
files.length > 1 || !fallbackFilePath || label !== fallbackFilePath;
const { additions, deletions } = countDiffFileChanges(file);
const diffType = normalizeDiffType(file.type);
const showFileHeader = shouldShowDiffFileHeader(
label,
files.length,
fallbackFilePath,
);
const fileKey = [
file.oldPath || "",
file.newPath || "",
Expand Down
Loading