fix(review): move hide-whitespace to server-side git diff -w (#635 follow-up) (#638)

The client-side approach from PR #635 normalized file contents before
diffing, which destroyed all indentation. Move whitespace handling to
the server by threading a `-w` flag through the git diff pipeline.

- Add `GitDiffOptions` to review-core.ts, inject `-w` in all 7 diff
  type paths + untracked file diffs
- Thread options through git.ts → vcs.ts → review.ts / Pi server
- Read `hideWhitespace` from ~/.plannotator/config.json on startup so
  the initial diff already respects the persisted preference
- Accept `hideWhitespace` in `/api/diff/switch`, echo in responses
- Client toggles trigger a lightweight server refetch that preserves
  the active file (no panel reset)
- Handle edge case where current file disappears when `-w` removes
  whitespace-only diffs
- Remove broken client-side parseDiffFromFile/normalize hack
- Update API docs in AGENTS.md

Closes the indentation bug reported by @zeroZshadow on PR #631.

For provenance purposes, this commit was AI assisted.
This commit is contained in:
Michael Ramos
2026-04-30 17:23:55 -07:00
committed by GitHub
parent 657cccaee0
commit 9b7c39d2a4
14 changed files with 152 additions and 82 deletions
+12 -3
View File
@@ -44,7 +44,7 @@ import {
transformClaudeFindings,
} from "./claude-review";
import { createTourSession, TOUR_EMPTY_OUTPUT_ERROR } from "./tour/tour-review";
import { saveConfig, detectGitUser, getServerConfig } from "./config";
import { loadConfig, saveConfig, detectGitUser, getServerConfig } from "./config";
import { type PRMetadata, type PRReviewFileComment, type PRStackTree, type PRListItem, fetchPR, fetchPRFileContent, fetchPRContext, submitPRReview, fetchPRViewedFiles, markPRFilesViewed, fetchPRStack, fetchPRList, getPRUser, parsePRUrl, prRefFromMetadata, isSameProject, getDisplayRepo, getMRLabel, getMRNumberLabel } from "./pr";
import { createAIEndpoints, ProviderRegistry, SessionManager, createProvider, type AIEndpoints, type PiSDKConfig } from "@plannotator/ai";
import { isWSL } from "./browser";
@@ -150,6 +150,7 @@ export async function startReviewServer(
let currentGitRef = options.gitRef;
let currentDiffType: DiffType = options.diffType || "uncommitted";
let currentError = options.error;
let currentHideWhitespace = loadConfig().diffOptions?.hideWhitespace ?? false;
let originalPRPatch = options.rawPatch;
let originalPRGitRef = options.gitRef;
let originalPRError = options.error;
@@ -515,6 +516,7 @@ export async function startReviewServer(
// the picker to what the server is actually using — not the
// detected default.
base: hasLocalAccess ? currentBase : undefined,
hideWhitespace: currentHideWhitespace,
gitContext: hasLocalAccess ? gitContext : undefined,
sharingEnabled,
shareBaseUrl,
@@ -544,7 +546,7 @@ export async function startReviewServer(
);
}
try {
const body = (await req.json()) as { diffType: DiffType; base?: string };
const body = (await req.json()) as { diffType: DiffType; base?: string; hideWhitespace?: boolean };
let newDiffType = body.diffType;
if (!newDiffType) {
@@ -554,6 +556,10 @@ export async function startReviewServer(
);
}
if (typeof body.hideWhitespace === "boolean") {
currentHideWhitespace = body.hideWhitespace;
}
const detectedBase = gitContext?.defaultBranch || "main";
// Guard against non-string payloads — resolveBaseBranch calls
// string methods and would throw a TypeError otherwise. Mirrors
@@ -563,7 +569,9 @@ export async function startReviewServer(
const defaultCwd = gitContext?.cwd;
// Run the new diff
const result = await runVcsDiff(newDiffType, base, defaultCwd);
const result = await runVcsDiff(newDiffType, base, defaultCwd, {
hideWhitespace: currentHideWhitespace,
});
// Update state
currentPatch = result.patch;
@@ -597,6 +605,7 @@ export async function startReviewServer(
// confirm the request landed (and pick it up when the client
// didn't supply one and we fell back to detected default).
base: currentBase,
hideWhitespace: currentHideWhitespace,
...(updatedContext && { gitContext: updatedContext }),
...(currentError && { error: currentError }),
});