diff --git a/src/ui/components/panes/FileListItem.tsx b/src/ui/components/panes/FileListItem.tsx
index 9f5fe528..36276e93 100644
--- a/src/ui/components/panes/FileListItem.tsx
+++ b/src/ui/components/panes/FileListItem.tsx
@@ -1,7 +1,7 @@
-import type { FileGroupEntry, FileListEntry } from "../../lib/files";
+import { fileRowId } from "../../lib/ids";
+import { sidebarEntryStats, type FileGroupEntry, type FileListEntry } from "../../lib/files";
import { fitText, padText } from "../../lib/text";
import type { AppTheme } from "../../themes";
-import { fileRowId } from "../../lib/ids";
/** Get icon and color for file state using standard git status codes. */
function getFileStateIcon(entry: FileListEntry, theme: AppTheme): { icon: string; color: string } {
@@ -50,27 +50,26 @@ export function FileGroupHeader({
/** Render one file row in the navigation sidebar. */
export function FileListItem({
- additionsWidth,
- deletionsWidth,
entry,
selected,
+ statsWidth,
textWidth,
theme,
onSelect,
}: {
- additionsWidth: number;
- deletionsWidth: number;
entry: FileListEntry;
selected: boolean;
+ statsWidth: number;
textWidth: number;
theme: AppTheme;
onSelect: () => void;
}) {
const rowBackground = selected ? theme.panelAlt : theme.panel;
- const statsWidth = additionsWidth + 1 + deletionsWidth;
+ const stats = sidebarEntryStats(entry);
const { icon, color } = getFileStateIcon(entry, theme);
const iconWidth = icon ? 2 : 0; // icon + space
- const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsWidth - 1);
+ const statsSectionWidth = statsWidth > 0 ? statsWidth + 1 : 0;
+ const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsSectionWidth);
return (
{icon && {icon} }
{padText(fitText(entry.name, nameWidth), nameWidth)}
- {entry.additionsText.padStart(additionsWidth, " ")}
-
- {entry.deletionsText.padStart(deletionsWidth, " ")}
+ {statsSectionWidth > 0 && (
+
+ {stats.map((stat, index) => (
+
+ {index > 0 && }
+
+ {stat.text}
+
+
+ ))}
+
+ )}
);
diff --git a/src/ui/components/panes/SidebarPane.tsx b/src/ui/components/panes/SidebarPane.tsx
index ef3abb78..0391c25a 100644
--- a/src/ui/components/panes/SidebarPane.tsx
+++ b/src/ui/components/panes/SidebarPane.tsx
@@ -1,6 +1,6 @@
import type { ScrollBoxRenderable } from "@opentui/core";
import type { RefObject } from "react";
-import type { SidebarEntry } from "../../lib/files";
+import { sidebarEntryStatsWidth, type SidebarEntry } from "../../lib/files";
import type { AppTheme } from "../../themes";
import { FileGroupHeader, FileListItem } from "./FileListItem";
@@ -23,8 +23,7 @@ export function SidebarPane({
onSelectFile: (fileId: string) => void;
}) {
const fileEntries = entries.filter((entry) => entry.kind === "file");
- const additionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.additionsText.length));
- const deletionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.deletionsText.length));
+ const statsWidth = Math.max(0, ...fileEntries.map((entry) => sidebarEntryStatsWidth(entry)));
return (
onSelectFile(entry.id)}
diff --git a/src/ui/components/ui-components.test.tsx b/src/ui/components/ui-components.test.tsx
index ed865f66..fd42044e 100644
--- a/src/ui/components/ui-components.test.tsx
+++ b/src/ui/components/ui-components.test.tsx
@@ -345,15 +345,37 @@ describe("UI components", () => {
createTestDiffFile(
"menu",
"src/ui/MenuDropdown.tsx",
+ lines(
+ "export const menu = 1;",
+ "export const remove1 = true;",
+ "export const remove2 = true;",
+ "export const remove3 = true;",
+ ),
"export const menu = 1;\n",
- "export const menu = 2;\n",
),
createTestDiffFile(
"watch",
"src/core/watch.ts",
"export const watch = 1;\n",
- "export const watch = 2;\nexport const enabled = true;\n",
+ lines(
+ "export const watch = 1;",
+ "export const add1 = true;",
+ "export const add2 = true;",
+ "export const add3 = true;",
+ "export const add4 = true;",
+ "export const add5 = true;",
+ ),
),
+ {
+ ...createTestDiffFile(
+ "rename",
+ "src/ui/Renamed.tsx",
+ "export const renamed = true;\n",
+ "export const renamed = true;\n",
+ ),
+ previousPath: "src/ui/Legacy.tsx",
+ stats: { additions: 0, deletions: 0 },
+ },
];
const frame = await captureFrame(
{
expect(frame).toContain(" MenuDropdown.tsx");
expect(frame).toContain(" watch.ts");
expect(frame).toContain("+2 -1");
- expect(frame).toContain("+1 -1");
+ expect(frame).toContain("+5");
+ expect(frame).toContain("-3");
+ expect(frame).not.toContain("+0");
+ expect(frame).not.toContain("-0");
expect(frame).not.toContain("M +2 -1 AI");
});
diff --git a/src/ui/lib/files.test.ts b/src/ui/lib/files.test.ts
new file mode 100644
index 00000000..00a62762
--- /dev/null
+++ b/src/ui/lib/files.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, test } from "bun:test";
+import { createTestDiffFile, lines } from "../../../test/helpers/diff-helpers";
+import { buildSidebarEntries } from "./files";
+
+describe("files helpers", () => {
+ test("buildSidebarEntries hides zero-value sidebar stats", () => {
+ const onlyAdd = createTestDiffFile({
+ id: "only-add",
+ path: "src/ui/only-add.ts",
+ before: lines("export const stable = true;"),
+ after: lines(
+ "export const stable = true;",
+ "export const add1 = 1;",
+ "export const add2 = 2;",
+ "export const add3 = 3;",
+ "export const add4 = 4;",
+ "export const add5 = 5;",
+ ),
+ });
+ const onlyRemove = createTestDiffFile({
+ id: "only-remove",
+ path: "src/ui/only-remove.ts",
+ before: lines(
+ "export const stable = true;",
+ "export const remove1 = 1;",
+ "export const remove2 = 2;",
+ "export const remove3 = 3;",
+ ),
+ after: lines("export const stable = true;"),
+ });
+ const renamedWithoutContentChanges = {
+ ...createTestDiffFile({
+ id: "rename-only",
+ path: "src/ui/Renamed.tsx",
+ previousPath: "src/ui/Legacy.tsx",
+ before: lines("export const stable = true;"),
+ after: lines("export const stable = true;"),
+ }),
+ stats: { additions: 0, deletions: 0 },
+ };
+
+ const entries = buildSidebarEntries([onlyAdd, onlyRemove, renamedWithoutContentChanges]).filter(
+ (entry) => entry.kind === "file",
+ );
+
+ expect(entries).toHaveLength(3);
+ expect(entries[0]).toMatchObject({
+ name: "only-add.ts",
+ additionsText: "+5",
+ deletionsText: null,
+ });
+ expect(entries[1]).toMatchObject({
+ name: "only-remove.ts",
+ additionsText: null,
+ deletionsText: "-3",
+ });
+ expect(entries[2]).toMatchObject({
+ name: "Legacy.tsx -> Renamed.tsx",
+ additionsText: null,
+ deletionsText: null,
+ });
+ });
+});
diff --git a/src/ui/lib/files.ts b/src/ui/lib/files.ts
index d877d837..86def67f 100644
--- a/src/ui/lib/files.ts
+++ b/src/ui/lib/files.ts
@@ -6,8 +6,8 @@ export interface FileListEntry {
kind: "file";
id: string;
name: string;
- additionsText: string;
- deletionsText: string;
+ additionsText: string | null;
+ deletionsText: string | null;
changeType: FileDiffMetadata["type"];
isUntracked: boolean;
}
@@ -31,6 +31,36 @@ function sidebarFileName(file: DiffFile) {
return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
}
+/** Hide zero-value file stats so the sidebar only shows real line deltas. */
+function formatSidebarStat(prefix: "+" | "-", value: number) {
+ return value > 0 ? `${prefix}${value}` : null;
+}
+
+/** Build the visible stats badges for one sidebar row. */
+export function sidebarEntryStats(entry: Pick) {
+ const stats: Array<{ kind: "addition" | "deletion"; text: string }> = [];
+
+ if (entry.additionsText) {
+ stats.push({ kind: "addition", text: entry.additionsText });
+ }
+
+ if (entry.deletionsText) {
+ stats.push({ kind: "deletion", text: entry.deletionsText });
+ }
+
+ return stats;
+}
+
+/** Measure the rendered sidebar stats width, including the space between badges. */
+export function sidebarEntryStatsWidth(
+ entry: Pick,
+) {
+ return sidebarEntryStats(entry).reduce(
+ (width, stat, index) => width + stat.text.length + (index > 0 ? 1 : 0),
+ 0,
+ );
+}
+
/** Merge one file-id keyed annotation map into the review stream file list. */
export function mergeFileAnnotationsByFileId(
files: DiffFile[],
@@ -93,8 +123,8 @@ export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] {
kind: "file",
id: file.id,
name: sidebarFileName(file),
- additionsText: `+${file.stats.additions}`,
- deletionsText: `-${file.stats.deletions}`,
+ additionsText: formatSidebarStat("+", file.stats.additions),
+ deletionsText: formatSidebarStat("-", file.stats.deletions),
changeType: file.metadata.type,
isUntracked: file.isUntracked ?? false,
});