feat(review): add jj review workflows (#675)

* feat(review): add jj support for local diffs

* feat(review): add jj review workflows

* fix(review): tighten jj diff defaults

* test(review): add jj manual sandbox

* fix(review): share jj agent diff prompts

* fix(review): quote jj agent revsets

* feat(review): share jj vcs handling with pi

* fix(review): tighten jj bookmark and pi pr handling

* fix(review): tighten jj defaults and detection

* fix(review): harden jj diff and vcs detection

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
This commit is contained in:
Graeme Folk
2026-05-07 20:57:33 -06:00
committed by GitHub
parent 0e4d301925
commit 69ef11bdfb
47 changed files with 3465 additions and 657 deletions
+26 -19
View File
@@ -11,8 +11,8 @@
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
import type { Origin } from "@plannotator/shared/agents";
import { type DiffType, type GitContext, runVcsDiff, getVcsFileContentsForDiff, canStageFiles, stageFile, unstageFile, resolveVcsCwd, validateFilePath, getVcsContext, gitRuntime } from "./vcs";
import { parseWorktreeDiffType, detectRemoteDefaultBranch, resolveBaseBranch } from "@plannotator/shared/review-core";
import { type DiffType, type GitContext, runVcsDiff, getVcsFileContentsForDiff, canStageFiles, stageFile, unstageFile, resolveVcsCwd, validateFilePath, getVcsContext, detectRemoteDefaultCompareTarget, gitRuntime } from "./vcs";
import { parseWorktreeDiffType, resolveBaseBranch } from "@plannotator/shared/review-core";
import {
getPRDiffScopeOptions,
getPRStackInfo,
@@ -31,12 +31,12 @@ import { createExternalAnnotationHandler } from "./external-annotations";
import { createAgentJobHandler } from "./agent-jobs";
import {
CODEX_REVIEW_SYSTEM_PROMPT,
buildCodexReviewUserMessage,
buildCodexCommand,
generateOutputPath,
parseCodexOutput,
transformReviewFindings,
} from "./codex-review";
import { buildAgentReviewUserMessage } from "./agent-review-message";
import {
CLAUDE_REVIEW_PROMPT,
buildClaudeCommand,
@@ -139,6 +139,7 @@ export async function startReviewServer(
let prMetadata = options.prMetadata;
const isPRMode = !!prMetadata;
const hasLocalAccess = !!gitContext;
const sessionVcsType = gitContext?.vcsType;
let draftKey = contentHash(options.rawPatch);
const editorAnnotations = createEditorAnnotationHandler();
const externalAnnotations = createExternalAnnotationHandler("review");
@@ -164,15 +165,20 @@ export async function startReviewServer(
// read this (not gitContext.defaultBranch) so they analyze the same diff
// the reviewer is currently looking at. Honors an explicit initialBase from
// the caller — e.g. programmatic Pi callers can request a non-detected base.
let currentBase = options.initialBase || gitContext?.defaultBranch || "main";
const detectedCompareTarget = (): string => gitContext?.defaultBranch || gitContext?.compareTarget?.fallback || "main";
let currentBase = options.initialBase || detectedCompareTarget();
let baseEverSwitched = false;
const resolveReviewBase = (requestedBase?: string): string => {
return resolveBaseBranch(requestedBase, detectedCompareTarget());
};
// Fire-and-forget: query the remote for its actual default branch. If it
// arrives before the user interacts, quietly upgrade currentBase from the
// local fallback (e.g. "main") to the upstream ref (e.g. "origin/main").
// Non-blocking — the server is already listening by the time this resolves.
if (gitContext && !options.initialBase && !isPRMode) {
detectRemoteDefaultBranch(gitRuntime, gitContext.cwd).then((remote) => {
detectRemoteDefaultCompareTarget(gitContext.cwd, sessionVcsType).then((remote) => {
if (remote && !baseEverSwitched) currentBase = remote;
});
}
@@ -225,7 +231,7 @@ export async function startReviewServer(
return built ? { ...built, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext } : built;
}
const userMessage = buildCodexReviewUserMessage(currentPatch, currentDiffType, userMessageOptions, prMetadata);
const userMessage = buildAgentReviewUserMessage(currentPatch, currentDiffType, userMessageOptions, prMetadata);
if (provider === "codex") {
const model = typeof config?.model === "string" && config.model ? config.model : undefined;
@@ -415,6 +421,12 @@ export async function startReviewServer(
let repoInfo = isPRMode && prMetadata
? { display: getDisplayRepo(prMetadata), branch: `${getMRLabel(prMetadata)} ${getMRNumberLabel(prMetadata)}` }
: await getRepoInfo();
if (gitContext?.repository?.displayFallback) {
repoInfo = {
...repoInfo,
display: repoInfo?.display || gitContext.repository.displayFallback,
};
}
// Fetch current platform user (for own-PR/MR detection)
let prRef = isPRMode && prMetadata ? prRefFromMetadata(prMetadata) : null;
@@ -560,12 +572,11 @@ export async function startReviewServer(
currentHideWhitespace = body.hideWhitespace;
}
const detectedBase = gitContext?.defaultBranch || "main";
// Guard against non-string payloads — resolveBaseBranch calls
// string methods and would throw a TypeError otherwise. Mirrors
// Pi's guard so both runtimes validate identically.
const requestedBase = typeof body.base === "string" ? body.base : undefined;
const base = resolveBaseBranch(requestedBase, detectedBase);
const base = resolveReviewBase(requestedBase);
const defaultCwd = gitContext?.cwd;
// Run the new diff
@@ -590,7 +601,7 @@ export async function startReviewServer(
if (gitContext) {
try {
const effectiveCwd = resolveVcsCwd(newDiffType, gitContext.cwd);
updatedContext = await getVcsContext(effectiveCwd);
updatedContext = await getVcsContext(effectiveCwd, sessionVcsType);
} catch {
/* best-effort */
}
@@ -863,11 +874,8 @@ export async function startReviewServer(
// Local review: read file contents from local git
if (hasLocalAccess) {
const detectedBase = gitContext?.defaultBranch || "main";
const base = resolveBaseBranch(
url.searchParams.get("base") ?? undefined,
detectedBase,
);
const requestedBase = url.searchParams.get("base") ?? undefined;
const base = resolveReviewBase(requestedBase);
const defaultCwd = gitContext?.cwd;
const result = await getVcsFileContentsForDiff(
currentDiffType,
@@ -896,7 +904,8 @@ export async function startReviewServer(
// API: Stage / unstage a file (disabled when VCS doesn't support it)
if (url.pathname === "/api/git-add" && req.method === "POST") {
if (isPRMode || !canStageFiles(currentDiffType)) {
const stageCwd = resolveVcsCwd(currentDiffType, gitContext?.cwd);
if (isPRMode || !(await canStageFiles(currentDiffType, stageCwd))) {
return Response.json(
{ error: "Staging not available" },
{ status: 400 },
@@ -908,12 +917,10 @@ export async function startReviewServer(
return Response.json({ error: "Missing filePath" }, { status: 400 });
}
const cwd = resolveVcsCwd(currentDiffType, gitContext?.cwd);
if (body.undo) {
await unstageFile(currentDiffType, body.filePath, cwd);
await unstageFile(currentDiffType, body.filePath, stageCwd);
} else {
await stageFile(currentDiffType, body.filePath, cwd);
await stageFile(currentDiffType, body.filePath, stageCwd);
}
return Response.json({ ok: true });