Skip to content

Commit 5265377

Browse files
authored
feat: add file state indicators to sidebar (#128)
1 parent 9b030ac commit 5265377

7 files changed

Lines changed: 104 additions & 10 deletions

File tree

src/core/loaders.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ function buildDiffFile(
139139
index: number,
140140
sourcePrefix: string,
141141
agentContext: AgentContext | null,
142+
isUntracked?: boolean,
142143
previousPath?: string,
143144
): DiffFile {
144145
return {
@@ -150,6 +151,7 @@ function buildDiffFile(
150151
stats: countDiffStats(metadata),
151152
metadata,
152153
agent: findAgentFileContext(agentContext, metadata.name, metadata.prevName),
154+
isUntracked,
153155
};
154156
}
155157

@@ -235,6 +237,7 @@ function buildUntrackedDiffFile(
235237
index,
236238
sourcePrefix,
237239
agentContext,
240+
true, // isUntracked
238241
);
239242
}
240243

@@ -363,7 +366,9 @@ async function loadFileDiffChangeset(
363366
sourceLabel: input.kind === "difftool" ? "git difftool" : "file compare",
364367
title,
365368
agentSummary: agentContext?.summary,
366-
files: [buildDiffFile(metadata, patch, 0, displayPath, agentContext, basename(input.left))],
369+
files: [
370+
buildDiffFile(metadata, patch, 0, displayPath, agentContext, undefined, basename(input.left)),
371+
],
367372
} satisfies Changeset;
368373
}
369374

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface DiffFile {
3939
};
4040
metadata: FileDiffMetadata;
4141
agent: AgentFileContext | null;
42+
isUntracked?: boolean;
4243
}
4344

4445
export interface Changeset {

src/ui/components/panes/DiffSection.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DiffFile, LayoutMode } from "../../../core/types";
33
import { PierreDiffView } from "../../diff/PierreDiffView";
44
import { getAnnotatedHunkIndices, type VisibleAgentNote } from "../../lib/agentAnnotations";
55
import { diffSectionId } from "../../lib/ids";
6-
import { fileLabel } from "../../lib/files";
6+
import { fileLabelParts } from "../../lib/files";
77
import { fitText } from "../../lib/text";
88
import type { AppTheme } from "../../themes";
99

@@ -52,6 +52,7 @@ function DiffSectionComponent({
5252
const additionsText = `+${file.stats.additions}`;
5353
const deletionsText = `-${file.stats.deletions}`;
5454
const annotatedHunkIndices = getAnnotatedHunkIndices(file);
55+
const { filename, stateLabel } = fileLabelParts(file);
5556

5657
return (
5758
<box
@@ -90,7 +91,12 @@ function DiffSectionComponent({
9091
onMouseUp={onSelect}
9192
>
9293
{/* Clicking the file header jumps the main stream selection without collapsing to a single-file view. */}
93-
<text fg={theme.text}>{fitText(fileLabel(file), headerLabelWidth)}</text>
94+
<box style={{ flexDirection: "row" }}>
95+
<text fg={theme.text}>
96+
{fitText(filename, Math.max(1, headerLabelWidth - (stateLabel?.length ?? 0)))}
97+
</text>
98+
{stateLabel && <text fg={theme.muted}>{stateLabel}</text>}
99+
</box>
94100
<box
95101
style={{
96102
width: headerStatsWidth,

src/ui/components/panes/DiffSectionPlaceholder.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DiffFile } from "../../../core/types";
22
import { diffSectionId } from "../../lib/ids";
3-
import { fileLabel } from "../../lib/files";
3+
import { fileLabelParts } from "../../lib/files";
44
import { fitText } from "../../lib/text";
55
import type { AppTheme } from "../../themes";
66

@@ -28,6 +28,7 @@ export function DiffSectionPlaceholder({
2828
}: DiffSectionPlaceholderProps) {
2929
const additionsText = `+${file.stats.additions}`;
3030
const deletionsText = `-${file.stats.deletions}`;
31+
const { filename, stateLabel } = fileLabelParts(file);
3132

3233
return (
3334
<box
@@ -64,7 +65,12 @@ export function DiffSectionPlaceholder({
6465
}}
6566
onMouseUp={onSelect}
6667
>
67-
<text fg={theme.text}>{fitText(fileLabel(file), headerLabelWidth)}</text>
68+
<box style={{ flexDirection: "row" }}>
69+
<text fg={theme.text}>
70+
{fitText(filename, Math.max(1, headerLabelWidth - (stateLabel?.length ?? 0)))}
71+
</text>
72+
{stateLabel && <text fg={theme.muted}>{stateLabel}</text>}
73+
</box>
6874
<box
6975
style={{
7076
width: headerStatsWidth,

src/ui/components/panes/FileListItem.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ import { fitText, padText } from "../../lib/text";
33
import type { AppTheme } from "../../themes";
44
import { fileRowId } from "../../lib/ids";
55

6+
/** Get icon and color for file state using standard git status codes. */
7+
function getFileStateIcon(entry: FileListEntry, theme: AppTheme): { icon: string; color: string } {
8+
if (entry.isUntracked) {
9+
return { icon: "?", color: theme.fileUntracked };
10+
}
11+
12+
switch (entry.changeType) {
13+
case "new":
14+
return { icon: "A", color: theme.fileNew };
15+
case "deleted":
16+
return { icon: "D", color: theme.fileDeleted };
17+
case "rename-pure":
18+
case "rename-changed":
19+
return { icon: "R", color: theme.fileRenamed };
20+
case "change":
21+
return { icon: "M", color: theme.fileModified };
22+
default:
23+
return { icon: "", color: theme.text };
24+
}
25+
}
26+
627
/** Render one folder header in the navigation sidebar. */
728
export function FileGroupHeader({
829
entry,
@@ -47,7 +68,9 @@ export function FileListItem({
4768
}) {
4869
const rowBackground = selected ? theme.panelAlt : theme.panel;
4970
const statsWidth = additionsWidth + 1 + deletionsWidth;
50-
const nameWidth = Math.max(1, textWidth - 1 - statsWidth - 1);
71+
const { icon, color } = getFileStateIcon(entry, theme);
72+
const iconWidth = icon ? 2 : 0; // icon + space
73+
const nameWidth = Math.max(1, textWidth - 1 - iconWidth - statsWidth - 1);
5174

5275
return (
5376
<box
@@ -76,6 +99,7 @@ export function FileListItem({
7699
backgroundColor: rowBackground,
77100
}}
78101
>
102+
{icon && <text fg={color}>{icon} </text>}
79103
<text fg={theme.text}>{padText(fitText(entry.name, nameWidth), nameWidth)}</text>
80104
<text fg={theme.badgeAdded}>{entry.additionsText.padStart(additionsWidth, " ")}</text>
81105
<text fg={selected ? theme.text : theme.muted}> </text>

src/ui/lib/files.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { basename, dirname } from "node:path/posix";
2+
import type { FileDiffMetadata } from "@pierre/diffs";
23
import type { AgentAnnotation, DiffFile } from "../../core/types";
34

45
export interface FileListEntry {
@@ -7,6 +8,8 @@ export interface FileListEntry {
78
name: string;
89
additionsText: string;
910
deletionsText: string;
11+
changeType: FileDiffMetadata["type"];
12+
isUntracked: boolean;
1013
}
1114

1215
export interface FileGroupEntry {
@@ -92,6 +95,8 @@ export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] {
9295
name: sidebarFileName(file),
9396
additionsText: `+${file.stats.additions}`,
9497
deletionsText: `-${file.stats.deletions}`,
98+
changeType: file.metadata.type,
99+
isUntracked: file.isUntracked ?? false,
95100
});
96101
});
97102

@@ -100,11 +105,33 @@ export function buildSidebarEntries(files: DiffFile[]): SidebarEntry[] {
100105

101106
/** Build the canonical file label used across headers and note cards. */
102107
export function fileLabel(file: DiffFile | undefined) {
108+
const { filename, stateLabel } = fileLabelParts(file);
109+
return stateLabel ? `${filename}${stateLabel}` : filename;
110+
}
111+
112+
/** Split file label into filename and state label for styled rendering. */
113+
export function fileLabelParts(file: DiffFile | undefined): {
114+
filename: string;
115+
stateLabel: string | null;
116+
} {
103117
if (!file) {
104-
return "No file selected";
118+
return { filename: "No file selected", stateLabel: null };
119+
}
120+
121+
const baseLabel =
122+
file.previousPath && file.previousPath !== file.path
123+
? `${file.previousPath} -> ${file.path}`
124+
: file.path;
125+
126+
// Determine state label for special cases
127+
let stateLabel: string | null = null;
128+
if (file.isUntracked) {
129+
stateLabel = " (untracked)";
130+
} else if (file.metadata.type === "new") {
131+
stateLabel = " (new)";
132+
} else if (file.metadata.type === "deleted") {
133+
stateLabel = " (deleted)";
105134
}
106135

107-
return file.previousPath && file.previousPath !== file.path
108-
? `${file.previousPath} -> ${file.path}`
109-
: file.path;
136+
return { filename: baseLabel, stateLabel };
110137
}

src/ui/themes.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export interface AppTheme {
2626
badgeAdded: string;
2727
badgeRemoved: string;
2828
badgeNeutral: string;
29+
fileNew: string;
30+
fileDeleted: string;
31+
fileRenamed: string;
32+
fileModified: string;
33+
fileUntracked: string;
2934
noteBorder: string;
3035
noteBackground: string;
3136
noteTitleBackground: string;
@@ -110,6 +115,11 @@ export const THEMES: AppTheme[] = [
110115
badgeAdded: "#88d39b",
111116
badgeRemoved: "#f0a0a0",
112117
badgeNeutral: "#a9b4bf",
118+
fileNew: "#88d39b",
119+
fileDeleted: "#f0a0a0",
120+
fileRenamed: "#e6cf98",
121+
fileModified: "#c49bff",
122+
fileUntracked: "#7fd1ff",
113123
noteBorder: "#c6a0ff",
114124
noteBackground: "#241c31",
115125
noteTitleBackground: "#322446",
@@ -154,6 +164,11 @@ export const THEMES: AppTheme[] = [
154164
badgeAdded: "#5ad188",
155165
badgeRemoved: "#ff8b8b",
156166
badgeNeutral: "#89a5d3",
167+
fileNew: "#5ad188",
168+
fileDeleted: "#ff8b8b",
169+
fileRenamed: "#ffd883",
170+
fileModified: "#b794f6",
171+
fileUntracked: "#7fd1ff",
157172
noteBorder: "#c49bff",
158173
noteBackground: "#211a36",
159174
noteTitleBackground: "#30234f",
@@ -198,6 +213,11 @@ export const THEMES: AppTheme[] = [
198213
badgeAdded: "#3f8d58",
199214
badgeRemoved: "#b4545b",
200215
badgeNeutral: "#8e7355",
216+
fileNew: "#3f8d58",
217+
fileDeleted: "#b4545b",
218+
fileRenamed: "#9f6c1f",
219+
fileModified: "#7d5bc4",
220+
fileUntracked: "#4a6890",
201221
noteBorder: "#7d5bc4",
202222
noteBackground: "#efe6ff",
203223
noteTitleBackground: "#e3d7ff",
@@ -242,6 +262,11 @@ export const THEMES: AppTheme[] = [
242262
badgeAdded: "#83d99d",
243263
badgeRemoved: "#ff9d8f",
244264
badgeNeutral: "#f1be9d",
265+
fileNew: "#83d99d",
266+
fileDeleted: "#ff9d8f",
267+
fileRenamed: "#ffd08f",
268+
fileModified: "#d8b4fe",
269+
fileUntracked: "#ffb07a",
245270
noteBorder: "#e1a3ff",
246271
noteBackground: "#311d36",
247272
noteTitleBackground: "#452650",

0 commit comments

Comments
 (0)