diff --git a/src/tui/approve/Approve.tsx b/src/tui/approve/Approve.tsx
index 3c74182..744ede7 100644
--- a/src/tui/approve/Approve.tsx
+++ b/src/tui/approve/Approve.tsx
@@ -16,8 +16,13 @@ export function Approve({ fileName, oldText, newText, onDecision }: ApproveProps
const hunks = React.useMemo(() => computeHunks(oldText, newText), [oldText, newText]);
const adds = hunks.filter((h) => h.kind === 'add').length;
const dels = hunks.filter((h) => h.kind === 'del').length;
+ const [sideBySide, setSideBySide] = React.useState(false);
useInput((input, key) => {
+ if (input === 'v') {
+ setSideBySide((v) => !v);
+ return;
+ }
if (input === 'y') return onDecision('accept');
if (input === 'n' || input === 'q' || key.escape) return onDecision('reject');
});
@@ -27,18 +32,24 @@ export function Approve({ fileName, oldText, newText, onDecision }: ApproveProps
─ Approve change? {fileName} ─── + {adds} lines, − {dels} lines ─
-
- {hunks.map((h, i) => (
-
- ))}
-
+ {sideBySide ? : }
- y accept n reject q reject & quit ESC reject
+ y accept n reject v toggle side-by-side q reject & quit ESC reject
);
}
+function UnifiedView({ hunks }: { hunks: Hunk[] }): React.ReactElement {
+ return (
+
+ {hunks.map((h, i) => (
+
+ ))}
+
+ );
+}
+
function HunkLine({ hunk }: { hunk: Hunk }): React.ReactElement {
const sigil = hunk.kind === 'add' ? '+' : hunk.kind === 'del' ? '-' : ' ';
const color = hunk.kind === 'add' ? 'green' : hunk.kind === 'del' ? 'red' : undefined;
@@ -49,3 +60,68 @@ function HunkLine({ hunk }: { hunk: Hunk }): React.ReactElement {
);
}
+
+function pairForSideBySide(hunks: Hunk[]): Array<[Hunk | null, Hunk | null]> {
+ const out: Array<[Hunk | null, Hunk | null]> = [];
+ let i = 0;
+ while (i < hunks.length) {
+ if (hunks[i].kind === 'ctx') {
+ out.push([hunks[i], hunks[i]]);
+ i++;
+ continue;
+ }
+ const dels: Hunk[] = [];
+ const adds: Hunk[] = [];
+ while (i < hunks.length && hunks[i].kind !== 'ctx') {
+ if (hunks[i].kind === 'del') dels.push(hunks[i]);
+ else adds.push(hunks[i]);
+ i++;
+ }
+ const max = Math.max(dels.length, adds.length);
+ for (let j = 0; j < max; j++) {
+ out.push([dels[j] ?? null, adds[j] ?? null]);
+ }
+ }
+ return out;
+}
+
+function SideBySide({ hunks }: { hunks: Hunk[] }): React.ReactElement {
+ const rows = React.useMemo(() => pairForSideBySide(hunks), [hunks]);
+ return (
+
+ {rows.map((row, i) => (
+
+ ))}
+
+ );
+}
+
+function SideBySideRow({ left, right }: { left: Hunk | null; right: Hunk | null }): React.ReactElement {
+ return (
+
+
+
+
+ │
+
+
+
+
+ );
+}
+
+function HalfLine({ hunk, side }: { hunk: Hunk | null; side: 'left' | 'right' }): React.ReactElement {
+ if (!hunk) return ;
+ const color =
+ hunk.kind === 'add' ? 'green' : hunk.kind === 'del' ? 'red' : undefined;
+ const sigil =
+ hunk.kind === 'add' ? '+' : hunk.kind === 'del' ? '-' : ' ';
+ // For ctx, show ' '; for del on left, show '-'; for add on right, show '+'.
+ // (Cross-cell pollution like 'add' on the left side shouldn't happen given pairForSideBySide.)
+ void side;
+ return (
+
+ {sigil} {String(hunk.lineNo).padStart(4, ' ')} {hunk.text}
+
+ );
+}
diff --git a/tests/tui/Approve.test.tsx b/tests/tui/Approve.test.tsx
index 129ebb5..e51c58d 100644
--- a/tests/tui/Approve.test.tsx
+++ b/tests/tui/Approve.test.tsx
@@ -61,6 +61,33 @@ describe('', () => {
expect(decision).toHaveBeenCalledWith('reject');
});
+ it('toggles to side-by-side on v and shows both halves', async () => {
+ const { stdin, lastFrame } = render(
+ {}} />
+ );
+ await flush();
+ expect(lastFrame()).not.toContain('│');
+ stdin.write('v');
+ await flush();
+ const out = lastFrame()!;
+ expect(out).toContain('│');
+ expect(out).toContain('counter : INT := 0;');
+ expect(out).toContain('counter : DINT := 0;');
+ });
+
+ it('toggles back to unified on a second v', async () => {
+ const { stdin, lastFrame } = render(
+ {}} />
+ );
+ await flush();
+ stdin.write('v');
+ await flush();
+ expect(lastFrame()).toContain('│');
+ stdin.write('v');
+ await flush();
+ expect(lastFrame()).not.toContain('│');
+ });
+
it('calls onDecision("reject") on escape', async () => {
const decision = vi.fn();
const { stdin } = render(