mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat(review): add semantic diff overview (#871)
* feat(review): add semantic diff overview * fix(review): address semantic diff review findings * fix(review): harden semantic diff fallback * fix(review): avoid repo-local sem execution * fix(review): normalize semantic diff cwd * test(review): cover semantic diff local cwd * fix(gitlab): fetch raw MR diffs * fix(review): harden sem path resolution * docs(review): add semantic diff handoff * feat(review): semantic diff cards, header sem badges, jump-to-line - Restyle the semantic panel as real bordered cards (shadcn surface) instead of ASCII box-drawing; centered column, grid-aligned entity rows. - Hide orphan (module-level) changes from the rows, matching sem's default; the count still surfaces in the summary line. - Add a 'sem · N' hover popover to each diff file header showing that file's semantic changes, reusing the same row component as the panel. - Clicking an entity (panel or popover) scrolls the diff to the lines via pierre's [data-selected-line]; centers only when off-screen so manual drag-selection isn't disturbed. - Extract shared rows/helpers into semanticDiffShared; share one cached /api/semantic-diff fetch across header badges. * feat(review): land on All files by default Semantic diff stays available via the file-tree nav entry and header badges, but it's no longer the initial landing view. * chore(review): drop dead change-symbol entries, log badge fetch failures - Remove unreachable 'moved'/'renamed' entries from the changeSymbols table (getChangeSymbol early-returns for those before the lookup). - Log a console.error once per patch when the file-header badge's semantic diff fetch fails or returns a non-ok status, so a systemic failure leaves a trace instead of every badge silently showing nothing. * feat(review): flatten semantic diff panel into grouped list - Dissolve the per-file bordered cards into flat sections: the file path is now a quiet underlined header (single hairline) with entity rows flush beneath, hierarchy from whitespace + hover tint instead of boxes. - Split the path into muted directory + emphasized filename for faster scanning. - Nudge the add/remove glyphs (⊕/⊖) ~15% larger, line-height pinned so rows don't grow; modified/rename/reorder glyphs unchanged.
This commit is contained in:
@@ -14,6 +14,16 @@ import type { Origin } from "@plannotator/shared/agents";
|
||||
import { type DiffType, type GitContext, runVcsDiff, getVcsFileContentsForDiff, canStageFiles, stageFile, unstageFile, resolveVcsCwd, validateFilePath, getVcsContext, detectRemoteDefaultCompareTarget, gitRuntime } from "./vcs";
|
||||
import { basename } from "node:path";
|
||||
import { parseWorktreeDiffType, resolveBaseBranch } from "@plannotator/shared/review-core";
|
||||
import {
|
||||
createDefaultSemanticDiffRuntime,
|
||||
getSemanticDiffAvailability,
|
||||
getSemanticDiffScratchCwd,
|
||||
runSemanticDiff,
|
||||
semanticDiffCacheKey,
|
||||
semanticDiffFileExtsFromSearchParams,
|
||||
SemanticDiffResponseCache,
|
||||
} from "@plannotator/shared/semantic-diff";
|
||||
import type { SemanticDiffAvailability, SemanticDiffResponse } from "@plannotator/shared/semantic-diff-types";
|
||||
import {
|
||||
getPRDiffScopeOptions,
|
||||
getPRStackInfo,
|
||||
@@ -205,6 +215,68 @@ export async function startReviewServer(
|
||||
if (!workspace) return undefined;
|
||||
return workspace.getPromptContext();
|
||||
};
|
||||
const semanticDiffScratchCwd = getSemanticDiffScratchCwd();
|
||||
const resolveSemanticDiffCwd = (): string => {
|
||||
if (workspace) return workspace.root;
|
||||
if (options.worktreePool && prMetadata) {
|
||||
const poolPath = options.worktreePool.resolve(prMetadata.url);
|
||||
if (poolPath) return poolPath;
|
||||
}
|
||||
if (options.agentCwd) return options.agentCwd;
|
||||
if (gitContext) {
|
||||
const vcsCwd = resolveVcsCwd(currentDiffType as DiffType, gitContext.cwd);
|
||||
if (vcsCwd) return vcsCwd;
|
||||
if (gitContext.cwd) return gitContext.cwd;
|
||||
}
|
||||
return semanticDiffScratchCwd;
|
||||
};
|
||||
const semanticDiffCache = new SemanticDiffResponseCache();
|
||||
const semanticDiffAvailabilityCache = new Map<string, Promise<SemanticDiffAvailability>>();
|
||||
|
||||
const createSemanticDiffRuntime = (cwd: string) => ({
|
||||
...createDefaultSemanticDiffRuntime(),
|
||||
cwd,
|
||||
});
|
||||
|
||||
const getSemanticDiffAvailabilityForCwd = (cwd: string): Promise<SemanticDiffAvailability> => {
|
||||
const cached = semanticDiffAvailabilityCache.get(cwd);
|
||||
if (cached) return cached;
|
||||
|
||||
const next: Promise<SemanticDiffAvailability> = getSemanticDiffAvailability(createSemanticDiffRuntime(cwd)).catch((error) => ({
|
||||
available: false,
|
||||
reason: "sem-probe-failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
semanticDiffAvailabilityCache.set(cwd, next);
|
||||
return next;
|
||||
};
|
||||
|
||||
const getSemanticDiffAdvert = async () => {
|
||||
const availability = await getSemanticDiffAvailabilityForCwd(resolveSemanticDiffCwd());
|
||||
return {
|
||||
available: availability.available,
|
||||
...(availability.semVersion && { semVersion: availability.semVersion }),
|
||||
...(availability.semSource && { semSource: availability.semSource }),
|
||||
};
|
||||
};
|
||||
|
||||
const getSemanticDiff = async (url: URL): Promise<SemanticDiffResponse> => {
|
||||
const cwd = resolveSemanticDiffCwd();
|
||||
const fileExts = semanticDiffFileExtsFromSearchParams(url.searchParams);
|
||||
const cacheKey = semanticDiffCacheKey({ rawPatch: currentPatch, cwd, fileExts });
|
||||
const cached = semanticDiffCache.get(cacheKey, currentPatch);
|
||||
if (cached) return cached;
|
||||
|
||||
const result = await runSemanticDiff(
|
||||
{ rawPatch: currentPatch, cwd, fileExts },
|
||||
createSemanticDiffRuntime(cwd),
|
||||
);
|
||||
if (result.status === "ok") {
|
||||
semanticDiffCache.set(cacheKey, currentPatch, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const agentJobs = createAgentJobHandler({
|
||||
mode: "review",
|
||||
getServerUrl: () => serverUrl,
|
||||
@@ -511,10 +583,16 @@ export async function startReviewServer(
|
||||
}),
|
||||
...(isPRMode && initialViewedFiles.length > 0 && { viewedFiles: initialViewedFiles }),
|
||||
...(currentError && { error: currentError }),
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
serverConfig: getServerConfig(gitUser),
|
||||
});
|
||||
}
|
||||
|
||||
// API: Get semantic diff content
|
||||
if (url.pathname === "/api/semantic-diff" && req.method === "GET") {
|
||||
return Response.json(await getSemanticDiff(url));
|
||||
}
|
||||
|
||||
// API: Switch diff type (requires local file access)
|
||||
if (url.pathname === "/api/diff/switch" && req.method === "POST") {
|
||||
if (!hasLocalAccess && !workspace) {
|
||||
@@ -556,6 +634,7 @@ export async function startReviewServer(
|
||||
diffOptions: workspace.diffOptions,
|
||||
hideWhitespace: currentHideWhitespace,
|
||||
...(currentError && { error: currentError }),
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -606,6 +685,7 @@ export async function startReviewServer(
|
||||
hideWhitespace: currentHideWhitespace,
|
||||
...(updatedContext && { gitContext: updatedContext }),
|
||||
...(currentError && { error: currentError }),
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
});
|
||||
} catch (err) {
|
||||
const message =
|
||||
@@ -636,6 +716,7 @@ export async function startReviewServer(
|
||||
gitRef: currentGitRef,
|
||||
prDiffScope: currentPRDiffScope,
|
||||
...(currentError && { error: currentError }),
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -663,6 +744,7 @@ export async function startReviewServer(
|
||||
rawPatch: currentPatch,
|
||||
gitRef: currentGitRef,
|
||||
prDiffScope: currentPRDiffScope,
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
});
|
||||
} catch (err) {
|
||||
const message =
|
||||
@@ -789,6 +871,7 @@ export async function startReviewServer(
|
||||
repoInfo,
|
||||
...(switchedViewedFiles.length > 0 && { viewedFiles: switchedViewedFiles }),
|
||||
...(currentError ? { error: currentError } : {}),
|
||||
semanticDiff: await getSemanticDiffAdvert(),
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to switch PR";
|
||||
|
||||
Reference in New Issue
Block a user