diffhub 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +17 -16
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +17 -16
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +6 -5
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/standalone/apps/web/.next/server/app/api/comments/route.js +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/api/comments/route.js.nft.json +1 -1
  24. package/.next/standalone/apps/web/.next/server/app/api/diff/route.js +4 -3
  25. package/.next/standalone/apps/web/.next/server/app/api/diff/route.js.nft.json +1 -1
  26. package/.next/standalone/apps/web/.next/server/app/api/discard/route.js +3 -3
  27. package/.next/standalone/apps/web/.next/server/app/api/discard/route.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/api/file/route.js +3 -3
  29. package/.next/standalone/apps/web/.next/server/app/api/file/route.js.nft.json +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/api/files/route.js +3 -3
  31. package/.next/standalone/apps/web/.next/server/app/api/files/route.js.nft.json +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/api/open/route.js +1 -1
  33. package/.next/standalone/apps/web/.next/server/app/api/open/route.js.nft.json +1 -1
  34. package/.next/standalone/apps/web/.next/server/app/favicon.ico/route.js +1 -1
  35. package/.next/standalone/apps/web/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  36. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  37. package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -15
  38. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  39. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -15
  40. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
  41. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +6 -5
  42. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  43. package/.next/standalone/apps/web/.next/server/app/page/react-loadable-manifest.json +1 -2
  44. package/.next/standalone/apps/web/.next/server/app/page.js +2 -2
  45. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/apps/web/.next/server/chunks/[externals]_shiki_wasm_0~fgmgp._.js +3 -0
  48. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__01.zj5h._.js +3 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05ejtyr._.js +3 -0
  50. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0e2dp4h._.js +3 -0
  51. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0egk6ui._.js +3 -0
  52. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0i6i-~n._.js +3 -0
  53. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0sv4hr9._.js +3 -0
  54. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0tbrp5x._.js +13 -0
  55. package/.next/standalone/apps/web/.next/server/chunks/_0r24f4c._.js +69 -0
  56. package/.next/standalone/apps/web/.next/server/chunks/node_modules_@pierre_theme_dist_pierre-dark_mjs_0ojo3_n._.js +3 -0
  57. package/.next/standalone/apps/web/.next/server/chunks/node_modules_@pierre_theme_dist_pierre-light_mjs_0pw9wwg._.js +3 -0
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/0fuv_@swc_helpers_cjs__interop_require_default_cjs_0ghzfn9._.js +3 -0
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0bit3~x._.js +3 -0
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__06b81~v._.js → [root-of-the-server]__0giwc4b._.js} +2 -2
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_0b_ykcu._.js → _0m1v4-9._.js} +2 -2
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0oc3qg_._.js +3 -3
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_08kf15u._.js → apps_web_0758ax4._.js} +2 -2
  64. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  65. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  66. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  67. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  68. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  69. package/.next/standalone/apps/web/.next/static/chunks/01f1ms~jsc5vh.js +138 -0
  70. package/.next/standalone/apps/web/.next/static/chunks/0c7yx0ttmhb4s.js +63 -0
  71. package/.next/standalone/apps/web/.next/static/chunks/0ogqv_xj2r0c6.js +1 -0
  72. package/.next/standalone/apps/web/.next/static/chunks/0qp8t.3t~v6um.js +1 -0
  73. package/.next/standalone/apps/web/.next/static/chunks/{0syypqto3~pe_.js → 0y0o261rjun_2.js} +8 -8
  74. package/.next/standalone/apps/web/.next/static/chunks/0y5z3t-z1c8ks.js.map +5 -0
  75. package/.next/standalone/apps/web/.next/static/chunks/130i667qy-j80.css +3 -0
  76. package/.next/standalone/apps/web/.next/static/chunks/15uwrard~z-l5.js +16 -0
  77. package/.next/standalone/apps/web/.next/static/chunks/17b.xoi.b6rcl.js +1 -0
  78. package/.next/standalone/apps/web/.next/static/chunks/turbopack-0_n_4n~_4no2a.js +1 -0
  79. package/.next/standalone/apps/web/.next/static/chunks/turbopack-worker-0sjn--fhq~1cg.js +1 -0
  80. package/.next/standalone/apps/web/.next/static/media/diffs.worker.09unk0quktc_5.ts +1 -0
  81. package/.next/standalone/apps/web/package.json +4 -4
  82. package/README.md +10 -3
  83. package/bin/diffhub.mjs +25 -1
  84. package/package.json +4 -4
  85. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05vwx85._.js +0 -3
  86. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__09vmjc2._.js +0 -3
  87. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0etmu4u._.js +0 -3
  88. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0g-_a7n._.js +0 -3
  89. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0gf_xk6._.js +0 -13
  90. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0mshgfw._.js +0 -3
  91. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0qixima._.js +0 -3
  92. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0f~hmsk._.js +0 -3
  93. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0khyzju._.js +0 -3
  94. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0l2mjim._.js +0 -70
  95. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0kwaklj._.js +0 -3
  96. package/.next/standalone/apps/web/.next/static/chunks/0-ci0c9di0qo8.js +0 -16
  97. package/.next/standalone/apps/web/.next/static/chunks/0.ae0sud8tm7k.js +0 -204
  98. package/.next/standalone/apps/web/.next/static/chunks/07ndp5x2cvqo..css +0 -3
  99. package/.next/standalone/apps/web/.next/static/chunks/0di~ntk7iivm4.js +0 -1
  100. package/.next/standalone/apps/web/.next/static/chunks/0pklg0nmdvay8.js +0 -1
  101. package/.next/standalone/apps/web/.next/static/chunks/0up9_7hiwl_dt.js +0 -1
  102. package/.next/standalone/apps/web/.next/static/chunks/0~984a88e4rp9.js +0 -204
  103. package/.next/standalone/apps/web/.next/static/chunks/13y8355z8m13w.js +0 -1
  104. package/.next/standalone/apps/web/AGENTS.md +0 -60
  105. package/.next/standalone/apps/web/CHANGELOG.md +0 -7
  106. package/.next/standalone/apps/web/CLAUDE.md +0 -1
  107. package/.next/standalone/apps/web/README.md +0 -78
  108. package/.next/standalone/apps/web/app/api/comments/route.ts +0 -20
  109. package/.next/standalone/apps/web/app/api/diff/route.ts +0 -15
  110. package/.next/standalone/apps/web/app/api/discard/route.ts +0 -15
  111. package/.next/standalone/apps/web/app/api/file/route.ts +0 -17
  112. package/.next/standalone/apps/web/app/api/files/route.ts +0 -14
  113. package/.next/standalone/apps/web/app/api/open/route.ts +0 -64
  114. package/.next/standalone/apps/web/app/favicon.ico +0 -0
  115. package/.next/standalone/apps/web/app/globals.css +0 -214
  116. package/.next/standalone/apps/web/app/layout.tsx +0 -52
  117. package/.next/standalone/apps/web/app/page.tsx +0 -6
  118. package/.next/standalone/apps/web/bin/diffhub.mjs +0 -147
  119. package/.next/standalone/apps/web/components/ContextMenu.tsx +0 -161
  120. package/.next/standalone/apps/web/components/DiffApp.tsx +0 -419
  121. package/.next/standalone/apps/web/components/DiffViewer.tsx +0 -565
  122. package/.next/standalone/apps/web/components/FileDiffHeader.tsx +0 -119
  123. package/.next/standalone/apps/web/components/FileList.tsx +0 -455
  124. package/.next/standalone/apps/web/components/KeyboardShortcutsDialog.tsx +0 -79
  125. package/.next/standalone/apps/web/components/SidebarHelpMenu.tsx +0 -86
  126. package/.next/standalone/apps/web/components/StatusBar.tsx +0 -212
  127. package/.next/standalone/apps/web/components/icons/file-status-icons.tsx +0 -48
  128. package/.next/standalone/apps/web/components/theme-provider.tsx +0 -12
  129. package/.next/standalone/apps/web/components/ui/button.tsx +0 -90
  130. package/.next/standalone/apps/web/components/ui/empty.tsx +0 -82
  131. package/.next/standalone/apps/web/components/ui/input.tsx +0 -18
  132. package/.next/standalone/apps/web/components/ui/kbd.tsx +0 -14
  133. package/.next/standalone/apps/web/components/ui/separator.tsx +0 -23
  134. package/.next/standalone/apps/web/components/ui/sheet.tsx +0 -109
  135. package/.next/standalone/apps/web/components/ui/sidebar.tsx +0 -700
  136. package/.next/standalone/apps/web/components/ui/skeleton.tsx +0 -9
  137. package/.next/standalone/apps/web/components/ui/toggle.tsx +0 -35
  138. package/.next/standalone/apps/web/components/ui/tooltip.tsx +0 -52
  139. package/.next/standalone/apps/web/components.json +0 -27
  140. package/.next/standalone/apps/web/lib/comments.ts +0 -52
  141. package/.next/standalone/apps/web/lib/export-comments.ts +0 -13
  142. package/.next/standalone/apps/web/lib/git.ts +0 -201
  143. package/.next/standalone/apps/web/lib/use-mobile.ts +0 -19
  144. package/.next/standalone/apps/web/lib/utils.ts +0 -5
  145. package/.next/standalone/apps/web/next.config.ts +0 -19
  146. package/.next/standalone/apps/web/oxfmt.config.ts +0 -6
  147. package/.next/standalone/apps/web/oxlint.config.ts +0 -13
  148. package/.next/standalone/apps/web/postcss.config.mjs +0 -7
  149. package/.next/standalone/apps/web/public/file.svg +0 -1
  150. package/.next/standalone/apps/web/public/glide-variable-italic.woff2 +0 -0
  151. package/.next/standalone/apps/web/public/glide-variable.woff2 +0 -0
  152. package/.next/standalone/apps/web/public/globe.svg +0 -1
  153. package/.next/standalone/apps/web/public/next.svg +0 -1
  154. package/.next/standalone/apps/web/public/operator-mono-book-italic.woff2 +0 -0
  155. package/.next/standalone/apps/web/public/operator-mono-book.woff2 +0 -0
  156. package/.next/standalone/apps/web/public/operator-mono-medium-italic.woff2 +0 -0
  157. package/.next/standalone/apps/web/public/operator-mono-medium.woff2 +0 -0
  158. package/.next/standalone/apps/web/public/vercel.svg +0 -1
  159. package/.next/standalone/apps/web/public/window.svg +0 -1
  160. package/.next/standalone/apps/web/tsconfig.json +0 -34
  161. /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → C1Ggd_ITpnb7yBHrIOODV}/_buildManifest.js +0 -0
  162. /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → C1Ggd_ITpnb7yBHrIOODV}/_clientMiddlewareManifest.js +0 -0
  163. /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → C1Ggd_ITpnb7yBHrIOODV}/_ssgManifest.js +0 -0
@@ -1,419 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useEffect, useDeferredValue, useRef, useState, startTransition } from "react";
4
- import { StatusBar } from "./StatusBar";
5
- import type { DiffMode } from "./StatusBar";
6
- import { FileList } from "./FileList";
7
- import { DiffViewer } from "./DiffViewer";
8
- import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
9
- import { Button } from "@/components/ui/button";
10
- import type { DiffFileStat } from "@/lib/git";
11
- import type { Comment, CommentTag } from "@/lib/comments";
12
-
13
- interface FilesData {
14
- files: DiffFileStat[];
15
- insertions: number;
16
- deletions: number;
17
- branch: string;
18
- baseBranch: string;
19
- }
20
-
21
- interface FileDiff {
22
- patch: string;
23
- baseBranch: string;
24
- mergeBase: string;
25
- branch: string;
26
- }
27
-
28
- interface MainPanelProps {
29
- filesData: FilesData | null;
30
- deferredFileDiff: FileDiff | null;
31
- layout: "split" | "stacked";
32
- comments: Comment[];
33
- onAddComment: (
34
- file: string,
35
- lineNumber: number,
36
- side: string,
37
- body: string,
38
- tag: CommentTag,
39
- ) => Promise<void>;
40
- onDeleteComment: (id: string) => Promise<void>;
41
- selectedFile: string | null;
42
- viewedFiles: Set<string>;
43
- onToggleViewed: (file: string) => void;
44
- repoPath: string;
45
- onDiscard: ((file: string) => Promise<void>) | undefined;
46
- }
47
-
48
- const Placeholder = ({ text, pulse = false }: { text: string; pulse?: boolean }) => (
49
- <div className="flex h-full items-center justify-center">
50
- <div className={`text-muted-foreground text-sm${pulse ? " animate-pulse" : ""}`}>{text}</div>
51
- </div>
52
- );
53
-
54
- const MainPanel = ({
55
- filesData,
56
- deferredFileDiff,
57
- layout,
58
- comments,
59
- onAddComment,
60
- onDeleteComment,
61
- selectedFile,
62
- viewedFiles,
63
- onToggleViewed,
64
- repoPath,
65
- onDiscard,
66
- }: MainPanelProps) => {
67
- if (filesData === null) {
68
- return <Placeholder text="Loading diff…" pulse />;
69
- }
70
- if (filesData.files.length === 0) {
71
- return <Placeholder text="No changes" />;
72
- }
73
- if (!deferredFileDiff) {
74
- return <Placeholder text="Loading diff…" pulse />;
75
- }
76
- return (
77
- <DiffViewer
78
- patch={deferredFileDiff.patch}
79
- mergeBase={deferredFileDiff.mergeBase}
80
- layout={layout}
81
- comments={comments}
82
- onAddComment={onAddComment}
83
- onDeleteComment={onDeleteComment}
84
- selectedFileId={selectedFile}
85
- fileStats={filesData.files}
86
- viewedFiles={viewedFiles}
87
- onToggleViewed={onToggleViewed}
88
- repoPath={repoPath}
89
- onDiscard={onDiscard}
90
- />
91
- );
92
- };
93
-
94
- const POLL_INTERVAL = 5000;
95
-
96
- export const DiffApp = ({ repoPath }: { repoPath: string }) => {
97
- const [filesData, setFilesData] = useState<FilesData | null>(null);
98
- const [comments, setComments] = useState<Comment[]>([]);
99
- const [selectedFile, setSelectedFile] = useState<string | null>(null);
100
- // Ref mirror of selectedFile — lets pollFiles read it without a stale closure
101
- const selectedFileRef = useRef<string | null>(null);
102
- const [fileDiff, setFileDiff] = useState<FileDiff | null>(null);
103
- // Per-file patch cache: avoids re-fetching when switching back to a previously viewed file
104
- const diffCacheRef = useRef<Map<string, FileDiff>>(new Map());
105
- const [layout, setLayout] = useState<"split" | "stacked">("split");
106
- const [filterQuery, setFilterQuery] = useState("");
107
- const [refreshing, setRefreshing] = useState(false);
108
- const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
109
- const [loadError, setLoadError] = useState<string | null>(null);
110
- // Diff mode — "all" shows mergeBase...HEAD, "uncommitted" shows git diff HEAD
111
- const [diffMode, setDiffMode] = useState<DiffMode>("all");
112
- // Ref so callbacks always read the latest mode without being recreated
113
- const diffModeRef = useRef<DiffMode>("all");
114
- // Fingerprint of last-seen file stats to detect real changes between polls
115
- const lastStatsRef = useRef<string | null>(null);
116
- // In-flight guard: don't start a new poll if previous is still running
117
- const fetchingRef = useRef(false);
118
- // Viewed files — persisted to localStorage per repo
119
- const [viewedFiles, setViewedFiles] = useState<Set<string>>(() => {
120
- if (typeof window === "undefined") {
121
- return new Set();
122
- }
123
- try {
124
- const stored = localStorage.getItem(`diffhub-viewed:${repoPath}`);
125
- return stored ? new Set(JSON.parse(stored) as string[]) : new Set();
126
- } catch {
127
- // empty
128
- return new Set();
129
- }
130
- });
131
-
132
- const toggleViewed = useCallback(
133
- (file: string) => {
134
- setViewedFiles((prev) => {
135
- const next = new Set(prev);
136
- if (next.has(file)) {
137
- next.delete(file);
138
- } else {
139
- next.add(file);
140
- }
141
- try {
142
- localStorage.setItem(`diffhub-viewed:${repoPath}`, JSON.stringify([...next]));
143
- } catch {
144
- // empty
145
- }
146
- return next;
147
- });
148
- },
149
- [repoPath],
150
- );
151
-
152
- // Deferred patch for DiffViewer — keeps sidebar responsive during large renders
153
- const deferredFileDiff = useDeferredValue(fileDiff);
154
-
155
- // Fetch the diff for a single file; uses local cache
156
- const fetchFileDiff = useCallback(async (file: string) => {
157
- const cached = diffCacheRef.current.get(file);
158
- if (cached) {
159
- setFileDiff(cached);
160
- return;
161
- }
162
- try {
163
- const mode = diffModeRef.current;
164
- const modeParam = mode === "uncommitted" ? "&mode=uncommitted" : "";
165
- const res = await fetch(`/api/diff?file=${encodeURIComponent(file)}${modeParam}`);
166
- if (!res.ok) {
167
- return;
168
- }
169
- const data = (await res.json()) as FileDiff;
170
- diffCacheRef.current.set(file, data);
171
- setFileDiff(data);
172
- } catch {
173
- // empty
174
- }
175
- }, []);
176
-
177
- const handleSelectFile = useCallback(
178
- (file: string) => {
179
- selectedFileRef.current = file;
180
- startTransition(() => setSelectedFile(file));
181
- fetchFileDiff(file);
182
- },
183
- [fetchFileDiff],
184
- );
185
-
186
- // Poll /api/files for change detection (lightweight) + /api/comments
187
- const pollFiles = useCallback(
188
- async (silent = false) => {
189
- if (fetchingRef.current) {
190
- return;
191
- }
192
- fetchingRef.current = true;
193
- if (!silent) {
194
- setRefreshing(true);
195
- }
196
- setLoadError(null);
197
-
198
- try {
199
- const mode = diffModeRef.current;
200
- const modeParam = mode === "uncommitted" ? "?mode=uncommitted" : "";
201
-
202
- const [filesRes, commentsRes] = await Promise.all([
203
- fetch(`/api/files${modeParam}`),
204
- fetch("/api/comments"),
205
- ]);
206
-
207
- if (!filesRes.ok) {
208
- const err = await filesRes.json().catch(() => ({ error: "Network error" }));
209
- setLoadError(err.error ?? "Failed to load files");
210
- return;
211
- }
212
-
213
- const [files, commentsData] = await Promise.all([
214
- filesRes.json() as Promise<FilesData>,
215
- commentsRes.json() as Promise<Comment[]>,
216
- ]);
217
-
218
- startTransition(() => {
219
- setFilesData(files);
220
- setComments(commentsData);
221
- });
222
- setLastUpdated(new Date());
223
-
224
- // Auto-select first file on initial load
225
- if (!selectedFileRef.current && files.files.length > 0) {
226
- const first = files.files[0].file;
227
- selectedFileRef.current = first;
228
- setSelectedFile(first);
229
- fetchFileDiff(first);
230
- }
231
-
232
- // Detect if file stats changed — if so, invalidate cache and re-fetch selected file
233
- const fingerprint = files.files
234
- .map((f) => `${f.file}:${f.insertions}:${f.deletions}`)
235
- .join("|");
236
- if (fingerprint !== lastStatsRef.current) {
237
- lastStatsRef.current = fingerprint;
238
- diffCacheRef.current.clear();
239
- const curr = selectedFileRef.current;
240
- if (curr) {
241
- fetchFileDiff(curr);
242
- }
243
- }
244
- } catch (error) {
245
- setLoadError(error instanceof Error ? error.message : String(error));
246
- } finally {
247
- if (!silent) {
248
- setRefreshing(false);
249
- }
250
- fetchingRef.current = false;
251
- }
252
- },
253
- [fetchFileDiff],
254
- );
255
-
256
- // Initial load
257
- useEffect(() => {
258
- pollFiles();
259
- }, [pollFiles]);
260
-
261
- // Polling — only /api/files, not the full patch
262
- useEffect(() => {
263
- const interval = setInterval(() => pollFiles(true), POLL_INTERVAL);
264
- return () => clearInterval(interval);
265
- }, [pollFiles]);
266
-
267
- // Keyboard shortcuts
268
- useEffect(() => {
269
- // oxlint-disable-next-line complexity
270
- const handleKey = (e: KeyboardEvent) => {
271
- if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) {
272
- return;
273
- }
274
-
275
- if (e.key === "s" && !e.metaKey && !e.ctrlKey) {
276
- setLayout((l) => (l === "split" ? "stacked" : "split"));
277
- }
278
- if (e.key === "/" && !e.metaKey && !e.ctrlKey) {
279
- e.preventDefault();
280
- document.querySelector<HTMLInputElement>('input[placeholder="Filter files…"]')?.focus();
281
- }
282
- if (e.key === "v" && !e.metaKey && !e.ctrlKey && selectedFile) {
283
- toggleViewed(selectedFile);
284
- }
285
- if (e.key === "r" && !e.metaKey && !e.ctrlKey) {
286
- pollFiles();
287
- }
288
- if (e.key === "j" || e.key === "k") {
289
- const files = filesData?.files ?? [];
290
- if (files.length === 0) {
291
- return;
292
- }
293
- const idx = files.findIndex((f) => f.file === selectedFile);
294
- const next = e.key === "j" ? files[idx + 1] : files[idx - 1];
295
- if (next) {
296
- handleSelectFile(next.file);
297
- }
298
- }
299
- };
300
- window.addEventListener("keydown", handleKey);
301
- return () => window.removeEventListener("keydown", handleKey);
302
- }, [filesData, selectedFile, toggleViewed, pollFiles, handleSelectFile]);
303
-
304
- const handleDiffModeChange = useCallback(
305
- (mode: DiffMode) => {
306
- // update ref immediately so next poll/fetch uses it
307
- diffModeRef.current = mode;
308
- setDiffMode(mode);
309
- diffCacheRef.current.clear();
310
- lastStatsRef.current = null;
311
- pollFiles(false);
312
- },
313
- [pollFiles],
314
- );
315
-
316
- const handleAddComment = useCallback(
317
- async (file: string, lineNumber: number, side: string, body: string, tag: CommentTag) => {
318
- const res = await fetch("/api/comments", {
319
- body: JSON.stringify({ body, file, lineNumber, side, tag }),
320
- headers: { "Content-Type": "application/json" },
321
- method: "POST",
322
- });
323
- if (res.ok) {
324
- const comment = (await res.json()) as Comment;
325
- setComments((prev) => [...prev, comment]);
326
- }
327
- },
328
- [],
329
- );
330
-
331
- const handleDeleteComment = useCallback(async (id: string) => {
332
- await fetch(`/api/comments?id=${id}`, { method: "DELETE" });
333
- setComments((prev) => prev.filter((c) => c.id !== id));
334
- }, []);
335
-
336
- const handleDiscard = useCallback(
337
- async (file: string) => {
338
- const res = await fetch("/api/discard", {
339
- body: JSON.stringify({ file }),
340
- headers: { "Content-Type": "application/json" },
341
- method: "POST",
342
- });
343
- if (res.ok) {
344
- // Bust cache for this file and re-poll
345
- diffCacheRef.current.delete(file);
346
- await pollFiles(false);
347
- }
348
- },
349
- [pollFiles],
350
- );
351
-
352
- if (loadError) {
353
- return (
354
- <SidebarProvider className="h-svh">
355
- <SidebarInset className="flex flex-col h-svh items-center justify-center gap-4">
356
- <div className="rounded-lg border border-destructive/30 bg-destructive/10 px-6 py-4 text-sm text-destructive max-w-md text-center">
357
- <p className="font-semibold mb-1">Failed to load diff</p>
358
- <p className="text-xs opacity-80">{loadError}</p>
359
- </div>
360
- <Button
361
- variant="outline"
362
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
363
- onClick={() => pollFiles()}
364
- >
365
- Retry
366
- </Button>
367
- </SidebarInset>
368
- </SidebarProvider>
369
- );
370
- }
371
-
372
- return (
373
- <SidebarProvider className="h-svh">
374
- <FileList
375
- files={filesData?.files ?? []}
376
- selectedFile={selectedFile}
377
- onSelectFile={handleSelectFile}
378
- comments={comments}
379
- repoPath={repoPath}
380
- filterQuery={filterQuery}
381
- onFilterChange={setFilterQuery}
382
- viewedFiles={viewedFiles}
383
- />
384
-
385
- <SidebarInset className="flex flex-col h-svh overflow-hidden">
386
- <StatusBar
387
- branch={filesData?.branch ?? "…"}
388
- baseBranch={filesData?.baseBranch ?? "main"}
389
- insertions={filesData?.insertions ?? 0}
390
- deletions={filesData?.deletions ?? 0}
391
- fileCount={filesData?.files.length ?? 0}
392
- refreshing={refreshing}
393
- lastUpdated={lastUpdated}
394
- comments={comments}
395
- diffMode={diffMode}
396
- onDiffModeChange={handleDiffModeChange}
397
- layout={layout}
398
- onLayoutChange={setLayout}
399
- />
400
-
401
- <div className="flex-1 overflow-hidden">
402
- <MainPanel
403
- filesData={filesData}
404
- deferredFileDiff={deferredFileDiff}
405
- layout={layout}
406
- comments={comments}
407
- onAddComment={handleAddComment}
408
- onDeleteComment={handleDeleteComment}
409
- selectedFile={selectedFile}
410
- viewedFiles={viewedFiles}
411
- onToggleViewed={toggleViewed}
412
- repoPath={repoPath}
413
- onDiscard={diffMode === "uncommitted" ? handleDiscard : undefined}
414
- />
415
- </div>
416
- </SidebarInset>
417
- </SidebarProvider>
418
- );
419
- };