g1t/apps/web/app/components/diff-view.tsx

166 lines5,403 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Diffs on attempts; hosted agent presented as the g1t agent1import { FileDiff as FileIcon, FileMinus, FilePlus } from "lucide-react";
2
3import type { Comparison, DiffLine, FileDiff } from "@g1t/contracts";
4
5import { EmptyState } from "./ui";
6
7const ROW_STYLES: Record<DiffLine["kind"], string> = {
8 context: "",
9 add: "bg-accent/10",
10 delete: "bg-danger/10",
11};
12
13const MARKERS: Record<DiffLine["kind"], string> = {
14 context: " ",
15 add: "+",
16 delete: "-",
17};
18
19/** `+12 −3` with a five-block bar, like the summary on a pull request. */
20function Stat({ additions, deletions }: { additions: number; deletions: number }) {
21 const total = additions + deletions;
22 const green = total === 0 ? 0 : Math.round((additions / total) * 5);
23 return (
24 <span className="flex shrink-0 items-center gap-2 font-mono text-xs">
25 <span className="text-accent">+{additions}</span>
26 <span className="text-danger">−{deletions}</span>
27 <span className="flex gap-px" aria-hidden="true">
28 {Array.from({ length: 5 }, (_, i) => (
29 <span
30 key={i}
31 className={`size-2 rounded-[2px] ${
32 total === 0 ? "bg-line" : i < green ? "bg-accent" : "bg-danger"
33 }`}
34 />
35 ))}
36 </span>
37 </span>
38 );
39}
40
41function File({ file }: { file: FileDiff }) {
42 const Icon =
43 file.status === "added" ? FilePlus : file.status === "deleted" ? FileMinus : FileIcon;
44 return (
45 <section
46 id={`file-${file.path}`}
47 className="scroll-mt-20 overflow-hidden rounded-xl border border-line"
48 >
49 <header className="flex items-center gap-3 border-b border-line bg-surface px-4 py-2.5">
50 <Icon
51 size={15}
52 className={
53 file.status === "added"
54 ? "text-accent"
55 : file.status === "deleted"
56 ? "text-danger"
57 : "text-faint"
58 }
59 />
60 <span className="min-w-0 grow truncate font-mono text-[0.8125rem]">
61 {file.path}
62 </span>
63 <Stat additions={file.additions} deletions={file.deletions} />
64 </header>
65 {file.binary ? (
66 <p className="px-4 py-6 text-sm text-muted">
67 Binary or large file; its contents are not shown.
68 </p>
69 ) : file.hunks.length === 0 ? (
70 <p className="px-4 py-6 text-sm text-muted">No line changes.</p>
71 ) : (
72 <div className="overflow-x-auto">
73 <table className="w-full border-collapse font-mono text-xs leading-5">
74 <tbody>
75 {file.hunks.map((hunk, index) => (
76 <HunkRows key={index} lines={hunk.lines} first={index === 0} />
77 ))}
78 </tbody>
79 </table>
80 </div>
81 )}
82 </section>
83 );
84}
85
86function HunkRows({ lines, first }: { lines: DiffLine[]; first: boolean }) {
87 return (
88 <>
89 {!first && (
90 <tr aria-hidden="true">
91 <td colSpan={4} className="border-y border-line bg-surface py-1 text-center text-faint">
92 ⋯
93 </td>
94 </tr>
95 )}
96 {lines.map((line, index) => (
97 <tr key={index} className={ROW_STYLES[line.kind]}>
98 <td className="w-10 px-2 text-right text-faint select-none">{line.old}</td>
99 <td className="w-10 px-2 text-right text-faint select-none">{line.new}</td>
100 <td
101 className={`w-5 text-center select-none ${
102 line.kind === "add"
103 ? "text-accent"
104 : line.kind === "delete"
105 ? "text-danger"
106 : "text-faint"
107 }`}
108 >
109 {MARKERS[line.kind]}
110 </td>
111 <td className="pr-4 whitespace-pre">{line.text}</td>
112 </tr>
113 ))}
114 </>
115 );
116}
117
118export function DiffView({ comparison }: { comparison: Comparison }) {
119 const { files, truncated } = comparison;
120 if (files.length === 0) {
121 return (
122 <EmptyState title="No changes yet">
123 Nothing has been pushed to this attempt's fork, or it matches the
124 repository it came from.
125 </EmptyState>
126 );
127 }
128 const additions = files.reduce((sum, file) => sum + file.additions, 0);
129 const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
130 return (
131 <div className="space-y-4">
132 <div className="flex items-center gap-3 text-sm text-muted">
133 <span>
134 <span className="font-medium text-fg">{files.length}</span>{" "}
135 {files.length === 1 ? "file" : "files"} changed
136 </span>
137 <Stat additions={additions} deletions={deletions} />
138 </div>
139 {files.length > 1 && (
140 <ul className="rounded-xl border border-line bg-surface p-2 text-sm">
141 {files.map((file) => (
142 <li key={file.path}>
143 <a
144 href={`#file-${file.path}`}
145 className="flex items-center gap-3 rounded-md px-2 py-1 hover:bg-raised"
146 >
147 <span className="min-w-0 grow truncate font-mono text-[0.8125rem]">
148 {file.path}
149 </span>
150 <Stat additions={file.additions} deletions={file.deletions} />
151 </a>
152 </li>
153 ))}
154 </ul>
155 )}
156 {files.map((file) => (
157 <File key={file.path} file={file} />
158 ))}
159 {truncated && (
160 <p className="text-center text-sm text-muted">
161 This change is too large to show in full.
162 </p>
163 )}
164 </div>
165 );
166}