diffhub 0.1.0 → 0.1.2

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