Skip to content

Commit cbe382d

Browse files
authored
refactor: split app shell and diff view helpers (#52)
* refactor: split app shell and diff view helpers * refactor: split menu builder from controller
1 parent 9cd7984 commit cbe382d

9 files changed

Lines changed: 1657 additions & 1411 deletions

File tree

src/ui/App.tsx

Lines changed: 93 additions & 330 deletions
Large diffs are not rendered by default.

src/ui/diff/PierreDiffView.tsx

Lines changed: 32 additions & 1081 deletions
Large diffs are not rendered by default.

src/ui/diff/agentNoteOverlay.tsx

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import type { AgentAnnotation, DiffFile } from "../../core/types";
2+
import { AgentCard } from "../components/panes/AgentCard";
3+
import { annotationLocationLabel, type VisibleAgentNote } from "../lib/agentAnnotations";
4+
import { buildAgentPopoverContent, resolveAgentPopoverPlacement } from "../lib/agentPopover";
5+
import type { AppTheme } from "../themes";
6+
import type { DiffRow } from "./pierre";
7+
8+
interface SelectedOverlayNote {
9+
anchorColumn: number;
10+
anchorKey: string;
11+
note: VisibleAgentNote;
12+
noteCount: number;
13+
noteIndex: number;
14+
}
15+
16+
/** Resolve the visual anchor line for an annotation when one exists. */
17+
function noteAnchor(annotation: AgentAnnotation) {
18+
if (annotation.newRange) {
19+
return {
20+
side: "new" as const,
21+
lineNumber: annotation.newRange[0],
22+
};
23+
}
24+
25+
if (annotation.oldRange) {
26+
return {
27+
side: "old" as const,
28+
lineNumber: annotation.oldRange[0],
29+
};
30+
}
31+
32+
return null;
33+
}
34+
35+
/** Check whether a rendered row is the visual anchor for a note. */
36+
function rowMatchesNote(row: Extract<DiffRow, { type: "split-line" | "stack-line" }>, note: VisibleAgentNote) {
37+
const anchor = noteAnchor(note.annotation);
38+
if (!anchor) {
39+
return false;
40+
}
41+
42+
if (row.type === "split-line") {
43+
return anchor.side === "new" ? row.right.lineNumber === anchor.lineNumber : row.left.lineNumber === anchor.lineNumber;
44+
}
45+
46+
return anchor.side === "new" ? row.cell.newLineNumber === anchor.lineNumber : row.cell.oldLineNumber === anchor.lineNumber;
47+
}
48+
49+
/** Resolve the rendered row for the currently visible popover note. */
50+
function findNoteAnchorRow(rows: DiffRow[], note: VisibleAgentNote, selectedHunkIndex: number, showHunkHeaders: boolean) {
51+
const selectedHunkRows = rows.filter((row) => row.hunkIndex === selectedHunkIndex);
52+
const lineRows = selectedHunkRows.filter(
53+
(row): row is Extract<DiffRow, { type: "split-line" | "stack-line" }> => row.type === "split-line" || row.type === "stack-line",
54+
);
55+
const headerRow = selectedHunkRows.find((row) => row.type === "hunk-header");
56+
const firstVisibleRow = showHunkHeaders ? headerRow ?? lineRows[0] : lineRows[0] ?? headerRow;
57+
58+
return lineRows.find((row) => rowMatchesNote(row, note)) ?? firstVisibleRow;
59+
}
60+
61+
/** Pick a horizontal anchor column for the floating note popover. */
62+
function noteAnchorColumn(
63+
row: DiffRow,
64+
note: VisibleAgentNote,
65+
width: number,
66+
lineNumberDigits: number,
67+
showLineNumbers: boolean,
68+
) {
69+
if (row.type === "split-line") {
70+
const markerWidth = 1;
71+
const separatorWidth = 1;
72+
const usableWidth = Math.max(0, width - markerWidth - separatorWidth);
73+
const leftWidth = Math.max(0, markerWidth + Math.floor(usableWidth / 2));
74+
const anchor = noteAnchor(note.annotation);
75+
return anchor?.side === "old" ? 1 : leftWidth + 1;
76+
}
77+
78+
if (row.type === "stack-line") {
79+
return showLineNumbers ? Math.max(2, lineNumberDigits + 4) : 2;
80+
}
81+
82+
return 2;
83+
}
84+
85+
/** Resolve the single visible popover note for the selected hunk. */
86+
export function buildSelectedOverlayNote(
87+
rows: DiffRow[],
88+
visibleAgentNotes: VisibleAgentNote[],
89+
selectedHunkIndex: number,
90+
showHunkHeaders: boolean,
91+
width: number,
92+
lineNumberDigits: number,
93+
showLineNumbers: boolean,
94+
) {
95+
if (visibleAgentNotes.length === 0 || selectedHunkIndex < 0) {
96+
return null;
97+
}
98+
99+
const note = visibleAgentNotes[0]!;
100+
const anchorRow = findNoteAnchorRow(rows, note, selectedHunkIndex, showHunkHeaders);
101+
if (!anchorRow) {
102+
return null;
103+
}
104+
105+
return {
106+
anchorKey: anchorRow.key,
107+
anchorColumn: noteAnchorColumn(anchorRow, note, width, lineNumberDigits, showLineNumbers),
108+
note,
109+
noteIndex: 0,
110+
noteCount: visibleAgentNotes.length,
111+
} satisfies SelectedOverlayNote;
112+
}
113+
114+
/** Render the framed floating popover for the currently visible agent note. */
115+
export function renderAgentPopover(
116+
selectedOverlayNote: SelectedOverlayNote | null,
117+
file: DiffFile,
118+
width: number,
119+
contentHeight: number,
120+
rowMetrics: Map<string, { height: number; top: number }>,
121+
theme: AppTheme,
122+
onDismissAgentNote?: (id: string) => void,
123+
) {
124+
if (!selectedOverlayNote) {
125+
return null;
126+
}
127+
128+
const noteWidth = Math.min(Math.max(34, Math.floor(width * 0.42)), Math.max(12, width - 2));
129+
const locationLabel = annotationLocationLabel(file, selectedOverlayNote.note.annotation);
130+
const popover = buildAgentPopoverContent({
131+
summary: selectedOverlayNote.note.annotation.summary,
132+
rationale: selectedOverlayNote.note.annotation.rationale,
133+
locationLabel,
134+
noteIndex: selectedOverlayNote.noteIndex,
135+
noteCount: selectedOverlayNote.noteCount,
136+
width: noteWidth,
137+
});
138+
const anchorMetric = rowMetrics.get(selectedOverlayNote.anchorKey);
139+
if (!anchorMetric) {
140+
return null;
141+
}
142+
143+
const placement = resolveAgentPopoverPlacement({
144+
anchorColumn: selectedOverlayNote.anchorColumn,
145+
anchorRowTop: anchorMetric.top,
146+
anchorRowHeight: anchorMetric.height,
147+
contentHeight,
148+
noteHeight: popover.height,
149+
noteWidth,
150+
viewportWidth: width,
151+
});
152+
153+
return (
154+
<box style={{ position: "absolute", top: placement.top, left: placement.left, zIndex: 20 }}>
155+
<AgentCard
156+
locationLabel={locationLabel}
157+
noteCount={selectedOverlayNote.noteCount}
158+
noteIndex={selectedOverlayNote.noteIndex}
159+
rationale={selectedOverlayNote.note.annotation.rationale}
160+
summary={selectedOverlayNote.note.annotation.summary}
161+
theme={theme}
162+
width={noteWidth}
163+
onClose={onDismissAgentNote ? () => onDismissAgentNote(selectedOverlayNote.note.id) : undefined}
164+
/>
165+
</box>
166+
);
167+
}

0 commit comments

Comments
 (0)