Skip to content

Commit 8c20d41

Browse files
authored
Hide zero-value sidebar file stats (#174)
1 parent 6d431a5 commit 8c20d41

5 files changed

Lines changed: 158 additions & 23 deletions

File tree

src/ui/components/panes/FileListItem.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { FileGroupEntry, FileListEntry } from "../../lib/files";
1+
import { fileRowId } from "../../lib/ids";
2+
import { sidebarEntryStats, type FileGroupEntry, type FileListEntry } from "../../lib/files";
23
import { fitText, padText } from "../../lib/text";
34
import type { AppTheme } from "../../themes";
4-
import { fileRowId } from "../../lib/ids";
55

66
/** Get icon and color for file state using standard git status codes. */
77
function getFileStateIcon(entry: FileListEntry, theme: AppTheme): { icon: string; color: string } {
@@ -50,27 +50,26 @@ export function FileGroupHeader({
5050

5151
/** Render one file row in the navigation sidebar. */
5252
export function FileListItem({
53-
additionsWidth,
54-
deletionsWidth,
5553
entry,
5654
selected,
55+
statsWidth,
5756
textWidth,
5857
theme,
5958
onSelect,
6059
}: {
61-
additionsWidth: number;
62-
deletionsWidth: number;
6360
entry: FileListEntry;
6461
selected: boolean;
62+
statsWidth: number;
6563
textWidth: number;
6664
theme: AppTheme;
6765
onSelect: () => void;
6866
}) {
6967
const rowBackground = selected ? theme.panelAlt : theme.panel;
70-
const statsWidth = additionsWidth + 1 + deletionsWidth;
68+
const stats = sidebarEntryStats(entry);
7169
const { icon, color } = getFileStateIcon(entry, theme);
7270
const iconWidth = icon ? 2 : 0; // icon + space
73-
const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsWidth - 1);
71+
const statsSectionWidth = statsWidth > 0 ? statsWidth + 1 : 0;
72+
const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsSectionWidth);
7473

7574
return (
7675
<box
@@ -101,9 +100,29 @@ export function FileListItem({
101100
>
102101
{icon && <text fg={color}>{icon} </text>}
103102
<text fg={theme.text}>{padText(fitText(entry.name, nameWidth), nameWidth)}</text>
104-
<text fg={theme.badgeAdded}>{entry.additionsText.padStart(additionsWidth, " ")}</text>
105-
<text fg={selected ? theme.text : theme.muted}> </text>
106-
<text fg={theme.badgeRemoved}>{entry.deletionsText.padStart(deletionsWidth, " ")}</text>
103+
{statsSectionWidth > 0 && (
104+
<box
105+
style={{
106+
width: statsSectionWidth,
107+
height: 1,
108+
flexDirection: "row",
109+
justifyContent: "flex-end",
110+
backgroundColor: rowBackground,
111+
}}
112+
>
113+
{stats.map((stat, index) => (
114+
<box
115+
key={`${entry.id}:${stat.kind}`}
116+
style={{ height: 1, flexDirection: "row", backgroundColor: rowBackground }}
117+
>
118+
{index > 0 && <text fg={selected ? theme.text : theme.muted}> </text>}
119+
<text fg={stat.kind === "addition" ? theme.badgeAdded : theme.badgeRemoved}>
120+
{stat.text}
121+
</text>
122+
</box>
123+
))}
124+
</box>
125+
)}
107126
</box>
108127
</box>
109128
);

src/ui/components/panes/SidebarPane.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ScrollBoxRenderable } from "@opentui/core";
22
import type { RefObject } from "react";
3-
import type { SidebarEntry } from "../../lib/files";
3+
import { sidebarEntryStatsWidth, type SidebarEntry } from "../../lib/files";
44
import type { AppTheme } from "../../themes";
55
import { FileGroupHeader, FileListItem } from "./FileListItem";
66

@@ -23,8 +23,7 @@ export function SidebarPane({
2323
onSelectFile: (fileId: string) => void;
2424
}) {
2525
const fileEntries = entries.filter((entry) => entry.kind === "file");
26-
const additionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.additionsText.length));
27-
const deletionsWidth = Math.max(2, ...fileEntries.map((entry) => entry.deletionsText.length));
26+
const statsWidth = Math.max(0, ...fileEntries.map((entry) => sidebarEntryStatsWidth(entry)));
2827

2928
return (
3029
<box
@@ -59,10 +58,9 @@ export function SidebarPane({
5958
) : (
6059
<FileListItem
6160
key={entry.id}
62-
additionsWidth={additionsWidth}
63-
deletionsWidth={deletionsWidth}
6461
entry={entry}
6562
selected={entry.id === selectedFileId}
63+
statsWidth={statsWidth}
6664
textWidth={textWidth}
6765
theme={theme}
6866
onSelect={() => onSelectFile(entry.id)}

src/ui/components/ui-components.test.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,37 @@ describe("UI components", () => {
345345
createTestDiffFile(
346346
"menu",
347347
"src/ui/MenuDropdown.tsx",
348+
lines(
349+
"export const menu = 1;",
350+
"export const remove1 = true;",
351+
"export const remove2 = true;",
352+
"export const remove3 = true;",
353+
),
348354
"export const menu = 1;\n",
349-
"export const menu = 2;\n",
350355
),
351356
createTestDiffFile(
352357
"watch",
353358
"src/core/watch.ts",
354359
"export const watch = 1;\n",
355-
"export const watch = 2;\nexport const enabled = true;\n",
360+
lines(
361+
"export const watch = 1;",
362+
"export const add1 = true;",
363+
"export const add2 = true;",
364+
"export const add3 = true;",
365+
"export const add4 = true;",
366+
"export const add5 = true;",
367+
),
356368
),
369+
{
370+
...createTestDiffFile(
371+
"rename",
372+
"src/ui/Renamed.tsx",
373+
"export const renamed = true;\n",
374+
"export const renamed = true;\n",
375+
),
376+
previousPath: "src/ui/Legacy.tsx",
377+
stats: { additions: 0, deletions: 0 },
378+
},
357379
];
358380
const frame = await captureFrame(
359381
<SidebarPane
@@ -375,7 +397,10 @@ describe("UI components", () => {
375397
expect(frame).toContain(" MenuDropdown.tsx");
376398
expect(frame).toContain(" watch.ts");
377399
expect(frame).toContain("+2 -1");
378-
expect(frame).toContain("+1 -1");
400+
expect(frame).toContain("+5");
401+
expect(frame).toContain("-3");
402+
expect(frame).not.toContain("+0");
403+
expect(frame).not.toContain("-0");
379404
expect(frame).not.toContain("M +2 -1 AI");
380405
});
381406

src/ui/lib/files.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { createTestDiffFile, lines } from "../../../test/helpers/diff-helpers";
3+
import { buildSidebarEntries } from "./files";
4+
5+
describe("files helpers", () => {
6+
test("buildSidebarEntries hides zero-value sidebar stats", () => {
7+
const onlyAdd = createTestDiffFile({
8+
id: "only-add",
9+
path: "src/ui/only-add.ts",
10+
before: lines("export const stable = true;"),
11+
after: lines(
12+
"export const stable = true;",
13+
"export const add1 = 1;",
14+
"export const add2 = 2;",
15+
"export const add3 = 3;",
16+
"export const add4 = 4;",
17+
"export const add5 = 5;",
18+
),
19+
});
20+
const onlyRemove = createTestDiffFile({
21+
id: "only-remove",
22+
path: "src/ui/only-remove.ts",
23+
before: lines(
24+
"export const stable = true;",
25+
"export const remove1 = 1;",
26+
"export const remove2 = 2;",
27+
"export const remove3 = 3;",
28+
),
29+
after: lines("export const stable = true;"),
30+
});
31+
const renamedWithoutContentChanges = {
32+
...createTestDiffFile({
33+
id: "rename-only",
34+
path: "src/ui/Renamed.tsx",
35+
previousPath: "src/ui/Legacy.tsx",
36+
before: lines("export const stable = true;"),
37+
after: lines("export const stable = true;"),
38+
}),
39+
stats: { additions: 0, deletions: 0 },
40+
};
41+
42+
const entries = buildSidebarEntries([onlyAdd, onlyRemove, renamedWithoutContentChanges]).filter(
43+
(entry) => entry.kind === "file",
44+
);
45+
46+
expect(entries).toHaveLength(3);
47+
expect(entries[0]).toMatchObject({
48+
name: "only-add.ts",
49+
additionsText: "+5",
50+
deletionsText: null,
51+
});
52+
expect(entries[1]).toMatchObject({
53+
name: "only-remove.ts",
54+
additionsText: null,
55+
deletionsText: "-3",
56+
});
57+
expect(entries[2]).toMatchObject({
58+
name: "Legacy.tsx -> Renamed.tsx",
59+
additionsText: null,
60+
deletionsText: null,
61+
});
62+
});
63+
});

src/ui/lib/files.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export interface FileListEntry {
66
kind: "file";
77
id: string;
88
name: string;
9-
additionsText: string;
10-
deletionsText: string;
9+
additionsText: string | null;
10+
deletionsText: string | null;
1111
changeType: FileDiffMetadata["type"];
1212
isUntracked: boolean;
1313
}
@@ -31,6 +31,36 @@ function sidebarFileName(file: DiffFile) {
3131
return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
3232
}
3333

34+
/** Hide zero-value file stats so the sidebar only shows real line deltas. */
35+
function formatSidebarStat(prefix: "+" | "-", value: number) {
36+
return value > 0 ? `${prefix}${value}` : null;
37+
}
38+
39+
/** Build the visible stats badges for one sidebar row. */
40+
export function sidebarEntryStats(entry: Pick<FileListEntry, "additionsText" | "deletionsText">) {
41+
const stats: Array<{ kind: "addition" | "deletion"; text: string }> = [];
42+
43+
if (entry.additionsText) {
44+
stats.push({ kind: "addition", text: entry.additionsText });
45+
}
46+
47+
if (entry.deletionsText) {
48+
stats.push({ kind: "deletion", text: entry.deletionsText });
49+
}
50+
51+
return stats;
52+
}
53+
54+
/** Measure the rendered sidebar stats width, including the space between badges. */
55+
export function sidebarEntryStatsWidth(
56+
entry: Pick<FileListEntry, "additionsText" | "deletionsText">,
57+
) {
58+
return sidebarEntryStats(entry).reduce(
59+
(width, stat, index) => width + stat.text.length + (index > 0 ? 1 : 0),
60+
0,
61+
);
62+
}
63+
3464
/** Merge one file-id keyed annotation map into the review stream file list. */
3565
export function mergeFileAnnotationsByFileId<T extends AgentAnnotation>(
3666
files: DiffFile[],
@@ -93,8 +123,8 @@ export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] {
93123
kind: "file",
94124
id: file.id,
95125
name: sidebarFileName(file),
96-
additionsText: `+${file.stats.additions}`,
97-
deletionsText: `-${file.stats.deletions}`,
126+
additionsText: formatSidebarStat("+", file.stats.additions),
127+
deletionsText: formatSidebarStat("-", file.stats.deletions),
98128
changeType: file.metadata.type,
99129
isUntracked: file.isUntracked ?? false,
100130
});

0 commit comments

Comments
 (0)