+ {/* Review progress */}
+ {totalCount > 0 && (
+
+
+ Review Progress
+
+
+
+ {viewedCount}/{totalCount} files viewed
-
-
- {comments
- .filter((c) => !c.parentCommentId)
- .map((thread) => {
- const replyCount = comments.filter(
- (c) => c.parentCommentId === thread.id,
- ).length;
- return (
-
- );
- })}
-
- )}
+ )}
- {/* Directory-grouped file tree */}
-
-
- Files
- {selectedCommit && (
-
- ({activeFiles.length} in commit)
-
- )}
-
- {directoryGroups.map((group) => (
-
- {/* Directory header */}
- {group.directory && (
-
-
- {group.directory}
-
- )}
- {/* Files in directory */}
+ {/* Discussions — individual threads */}
+ {comments.length > 0 && (
+
+
+ Discussions
+
+ {unresolvedCount > 0 && (
+
+
+ {unresolvedCount}
+
+ )}
+ {resolvedCount > 0 && (
+
+
+ {resolvedCount}
+
+ )}
+
+
- {group.files.map((file) => {
- const fileCommentCount = comments.filter(
- (c) => c.filePath === file.newPath && !c.parentCommentId,
- ).length;
- const isInView = activeFilePaths.has(file.newPath);
- const dimmed = selectedCommit && !isInView;
- const isViewed = viewedFiles.has(file.newPath);
- const dotColor = changeTypeDotColor[file.changeType];
-
- return (
-
- ))}
+ )}
+
+ {/* Files section heading */}
+
+
+ Files
+ {selectedCommit && (
+
+ ({activeFiles.length} in commit)
+
+ )}
+
+
+
+ {/* Scrollable file tree — virtualized (react-window 2.x List) when >50 rows */}
+ {isVirtualized ? (
+
+ ) : (
+
+ {directoryGroups.map((group) => (
+
+ {/* Directory header — collapsible */}
+ {group.directory && (
+
toggleDir(group.directory)}
+ title={collapsedDirs.has(group.directory) ? "Expand directory" : "Collapse directory"}
+ >
+
+
+ {group.directory}
+
+ )}
+ {/* Files in directory */}
+ {!collapsedDirs.has(group.directory) && (
+
+ {group.files.map((file) => {
+ const fileCommentCount = comments.filter(
+ (c) => c.filePath === file.newPath && !c.parentCommentId,
+ ).length;
+ const isInView = activeFilePaths.has(file.newPath);
+ const dimmed = selectedCommit && !isInView;
+ const isViewed = viewedFiles.has(file.newPath);
+ const dotColor = changeTypeDotColor[file.changeType];
+
+ return (
+
onFileClick(file.newPath)}
+ >
+ {isViewed ? (
+
+ ) : (
+
+ )}
+ {dotColor && (
+
+ )}
+
+ {getFileName(file.newPath)}
+
+
+ {fileCommentCount > 0 && (
+
+
+ {fileCommentCount}
+
+ )}
+ {file.additions > 0 && (
+
+
+ {file.additions}
+
+ )}
+ {file.deletions > 0 && (
+
+
+ {file.deletions}
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ ))}
+
+ )}
);
}
diff --git a/docs/frontend.md b/docs/frontend.md
index 0797687..63e1e0f 100644
--- a/docs/frontend.md
+++ b/docs/frontend.md
@@ -14,6 +14,7 @@
| Tiptap | Rich text editor (ProseMirror-based) |
| Lucide | Icon library |
| Geist Sans/Mono | Typography (variable fonts in `public/fonts/`) |
+| react-window 2.x | Virtualized list rendering for large file trees in ReviewSidebar |
## Design System (v2)
@@ -115,7 +116,7 @@ The initiative detail page has three tabs managed via local state (not URL param
|-----------|---------|
| `ReviewTab` | Review tab container — orchestrates header, diff, sidebar, and preview. Phase-level review has threaded inline comments (with reply support) + Request Changes; initiative-level review has Request Changes (summary prompt) + Push Branch / Merge & Push |
| `ReviewHeader` | Consolidated toolbar: phase selector pills, branch info, stats, preview controls, approve/reject actions |
-| `ReviewSidebar` | VSCode-style icon strip (Files/Commits views) with file list, root-only comment counts, and commit navigation |
+| `ReviewSidebar` | VSCode-style icon strip (Files/Commits views) with file list, root-only comment counts, and commit navigation. FilesView uses react-window 2.x `List` for virtualized rendering when the row count exceeds 50 (dir-headers + file rows). Scroll position is preserved across Files ↔ Commits tab switches. Directories are collapsible. Clicking a file scrolls the virtual list to that row. |
| `DiffViewer` | Unified diff renderer with threaded inline comments (root + reply threads) |
| `CommentThread` | Renders root comment with resolve/reopen + nested reply threads (agent replies styled with primary border). Inline reply form |
| `ConflictResolutionPanel` | Merge conflict detection + agent resolution in initiative review. Shows conflict files, spawns conflict agent, inline questions, re-check on completion |
diff --git a/package-lock.json b/package-lock.json
index 89c8514..ab9b297 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -73,12 +73,14 @@
"@tiptap/suggestion": "^3.19.0",
"@trpc/client": "^11.9.0",
"@trpc/react-query": "^11.9.0",
+ "@types/react-window": "^1.8.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"lucide-react": "^0.563.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-window": "^2.2.7",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tippy.js": "^6.3.7"
@@ -5198,6 +5200,15 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-window": {
+ "version": "1.8.8",
+ "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
+ "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -9131,6 +9142,16 @@
}
}
},
+ "node_modules/react-window": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
+ "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/vitest.config.ts b/vitest.config.ts
index 0b610f5..3b89372 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -8,6 +8,9 @@ export default defineConfig({
alias: {
'@': path.resolve(__dirname, './apps/web/src'),
},
+ // Deduplicate React to avoid "multiple copies" errors when packages
+ // installed in workspace sub-directories shadow the hoisted copies.
+ dedupe: ['react', 'react-dom'],
},
test: {
// Enable test globals (describe, it, expect without imports)