diff --git a/apps/web/src/components/review/CommitNav.tsx b/apps/web/src/components/review/CommitNav.tsx
deleted file mode 100644
index 9085c80..0000000
--- a/apps/web/src/components/review/CommitNav.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { GitCommitHorizontal, Plus, Minus, FileCode } from "lucide-react";
-import type { CommitInfo } from "./types";
-
-interface CommitNavProps {
- commits: CommitInfo[];
- selectedCommit: string | null; // null = "all changes"
- onSelectCommit: (hash: string | null) => void;
- isLoading: boolean;
-}
-
-export function CommitNav({
- commits,
- selectedCommit,
- onSelectCommit,
- isLoading,
-}: CommitNavProps) {
- if (isLoading || commits.length === 0) return null;
-
- return (
-
-
- {/* "All changes" pill — only when multiple commits */}
- {commits.length > 1 && (
- <>
-
onSelectCommit(null)}
- />
-
- >
- )}
-
- {/* Individual commit pills - most recent first */}
- {commits.map((commit) => (
- onSelectCommit(commit.hash)}
- isMono
- />
- ))}
-
-
- );
-}
-
-interface CommitPillProps {
- label: string;
- sublabel: string;
- stats?: { files: number; add: number; del: number };
- isActive: boolean;
- onClick: () => void;
- isMono?: boolean;
-}
-
-function CommitPill({
- label,
- sublabel,
- stats,
- isActive,
- onClick,
- isMono,
-}: CommitPillProps) {
- return (
-
- );
-}
-
-function truncateMessage(msg: string): string {
- const firstLine = msg.split("\n")[0];
- return firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine;
-}
diff --git a/apps/web/src/components/review/ReviewSidebar.tsx b/apps/web/src/components/review/ReviewSidebar.tsx
index 378e6a3..6da6d37 100644
--- a/apps/web/src/components/review/ReviewSidebar.tsx
+++ b/apps/web/src/components/review/ReviewSidebar.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
import {
MessageSquare,
FileCode,
@@ -5,8 +6,12 @@ import {
Minus,
Circle,
CheckCircle2,
+ GitCommitHorizontal,
+ Layers,
} from "lucide-react";
-import type { FileDiff, ReviewComment } from "./types";
+import type { FileDiff, ReviewComment, CommitInfo } from "./types";
+
+type SidebarView = "files" | "commits";
interface ReviewSidebarProps {
files: FileDiff[];
@@ -14,6 +19,8 @@ interface ReviewSidebarProps {
onFileClick: (filePath: string) => void;
selectedCommit: string | null;
activeFiles: FileDiff[];
+ commits: CommitInfo[];
+ onSelectCommit: (hash: string | null) => void;
}
export function ReviewSidebar({
@@ -22,11 +29,107 @@ export function ReviewSidebar({
onFileClick,
selectedCommit,
activeFiles,
+ commits,
+ onSelectCommit,
}: ReviewSidebarProps) {
+ const [view, setView] = useState("files");
+
+ return (
+
+ {/* Content panel */}
+
+ {view === "files" ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Icon strip — right edge */}
+
+ setView("files")}
+ />
+ setView("commits")}
+ badge={commits.length > 1 ? commits.length : undefined}
+ />
+
+
+ );
+}
+
+/* ─── Icon Tab ─────────────────────────────────────────── */
+
+function IconTab({
+ icon: Icon,
+ label,
+ active,
+ onClick,
+ badge,
+}: {
+ icon: typeof FileCode;
+ label: string;
+ active: boolean;
+ onClick: () => void;
+ badge?: number;
+}) {
+ return (
+
+ );
+}
+
+/* ─── Files View ───────────────────────────────────────── */
+
+function FilesView({
+ files,
+ comments,
+ onFileClick,
+ selectedCommit,
+ activeFiles,
+}: {
+ files: FileDiff[];
+ comments: ReviewComment[];
+ onFileClick: (filePath: string) => void;
+ selectedCommit: string | null;
+ activeFiles: FileDiff[];
+}) {
const unresolvedCount = comments.filter((c) => !c.resolved).length;
const resolvedCount = comments.filter((c) => c.resolved).length;
-
- // Build a set of files visible in the current diff view
const activeFilePaths = new Set(activeFiles.map((f) => f.newPath));
return (
@@ -113,9 +216,101 @@ export function ReviewSidebar({
);
}
-/** Show filename with parent directory for context */
+/* ─── Commits View ─────────────────────────────────────── */
+
+function CommitsView({
+ commits,
+ selectedCommit,
+ onSelectCommit,
+}: {
+ commits: CommitInfo[];
+ selectedCommit: string | null;
+ onSelectCommit: (hash: string | null) => void;
+}) {
+ if (commits.length === 0) {
+ return (
+
+ No commits
+
+ );
+ }
+
+ return (
+
+
+ Commits
+
+
+ {/* "All changes" — only when >1 commit */}
+ {commits.length > 1 && (
+
+ )}
+
+ {/* Commit list */}
+ {commits.map((commit) => {
+ const isActive = selectedCommit === commit.hash;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+/* ─── Helpers ──────────────────────────────────────────── */
+
function formatFilePath(path: string): string {
const parts = path.split("/");
if (parts.length <= 2) return path;
return parts.slice(-2).join("/");
}
+
+function truncateMessage(msg: string): string {
+ const firstLine = msg.split("\n")[0];
+ return firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine;
+}
diff --git a/apps/web/src/components/review/ReviewTab.tsx b/apps/web/src/components/review/ReviewTab.tsx
index 852e81d..c40b129 100644
--- a/apps/web/src/components/review/ReviewTab.tsx
+++ b/apps/web/src/components/review/ReviewTab.tsx
@@ -6,7 +6,6 @@ import { parseUnifiedDiff } from "./parse-diff";
import { DiffViewer } from "./DiffViewer";
import { ReviewSidebar } from "./ReviewSidebar";
import { ReviewHeader } from "./ReviewHeader";
-import { CommitNav } from "./CommitNav";
import type { ReviewComment, ReviewStatus, DiffLine } from "./types";
interface ReviewTabProps {
@@ -235,14 +234,6 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
preview={previewState}
/>
- {/* Commit navigation strip */}
-
-
{/* Main content area */}
{isDiffLoading ? (
@@ -278,6 +269,8 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
onFileClick={handleFileClick}
selectedCommit={selectedCommit}
activeFiles={files}
+ commits={commits}
+ onSelectCommit={setSelectedCommit}
/>
diff --git a/docs/frontend.md b/docs/frontend.md
index 9d952f7..523bdbf 100644
--- a/docs/frontend.md
+++ b/docs/frontend.md
@@ -111,10 +111,9 @@ The initiative detail page has three tabs managed via local state (not URL param
### Review Components (`src/components/review/`)
| Component | Purpose |
|-----------|---------|
-| `ReviewTab` | Review tab container — orchestrates header, commit nav, diff, sidebar, and preview |
+| `ReviewTab` | Review tab container — orchestrates header, diff, sidebar, and preview |
| `ReviewHeader` | Consolidated toolbar: phase selector pills, branch info, stats, preview controls, approve/reject actions |
-| `CommitNav` | Horizontal commit navigation strip — "All changes" + individual commit pills with stats |
-| `ReviewSidebar` | File list with comment counts, dimmed files when viewing single commit |
+| `ReviewSidebar` | VSCode-style icon strip (Files/Commits views) with file list, comment counts, and commit navigation |
| `DiffViewer` | Unified diff renderer with inline comments |
| `PreviewPanel` | Docker preview status: building/running/failed with start/stop (legacy, now integrated into ReviewHeader) |
| `ProposalCard` | Individual proposal display |