Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions src/ui/components/panes/FileListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 } {
Expand Down Expand Up @@ -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 (
<box
Expand Down Expand Up @@ -101,9 +100,29 @@ export function FileListItem({
>
{icon && <text fg={color}>{icon} </text>}
<text fg={theme.text}>{padText(fitText(entry.name, nameWidth), nameWidth)}</text>
<text fg={theme.badgeAdded}>{entry.additionsText.padStart(additionsWidth, " ")}</text>
<text fg={selected ? theme.text : theme.muted}> </text>
<text fg={theme.badgeRemoved}>{entry.deletionsText.padStart(deletionsWidth, " ")}</text>
{statsSectionWidth > 0 && (
<box
style={{
width: statsSectionWidth,
height: 1,
flexDirection: "row",
justifyContent: "flex-end",
backgroundColor: rowBackground,
}}
>
{stats.map((stat, index) => (
<box
key={`${entry.id}:${stat.kind}`}
style={{ height: 1, flexDirection: "row", backgroundColor: rowBackground }}
>
{index > 0 && <text fg={selected ? theme.text : theme.muted}> </text>}
<text fg={stat.kind === "addition" ? theme.badgeAdded : theme.badgeRemoved}>
{stat.text}
</text>
</box>
))}
</box>
)}
</box>
</box>
);
Expand Down
8 changes: 3 additions & 5 deletions src/ui/components/panes/SidebarPane.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 (
<box
Expand Down Expand Up @@ -59,10 +58,9 @@ export function SidebarPane({
) : (
<FileListItem
key={entry.id}
additionsWidth={additionsWidth}
deletionsWidth={deletionsWidth}
entry={entry}
selected={entry.id === selectedFileId}
statsWidth={statsWidth}
textWidth={textWidth}
theme={theme}
onSelect={() => onSelectFile(entry.id)}
Expand Down
31 changes: 28 additions & 3 deletions src/ui/components/ui-components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<SidebarPane
Expand All @@ -375,7 +397,10 @@ describe("UI components", () => {
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");
});

Expand Down
63 changes: 63 additions & 0 deletions src/ui/lib/files.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
38 changes: 34 additions & 4 deletions src/ui/lib/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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<FileListEntry, "additionsText" | "deletionsText">) {
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<FileListEntry, "additionsText" | "deletionsText">,
) {
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<T extends AgentAnnotation>(
files: DiffFile[],
Expand Down Expand Up @@ -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,
});
Expand Down
Loading