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

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