import * as fs from "node:fs"; import * as path from "node:path"; import * as vcs from "@oh-my-pi/pi-natives/vcs"; import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "@oh-my-pi/pi-tui/render/render-utils"; import { isRecord, prompt } from "@oh-my-pi/pi-utils"; import { findScopedSettings } from "../../../../config/settings"; import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types"; import type { HookCommandContext } from "../../../../extensibility/hooks/types"; import { parseMrUrl } from "../../../../internal-urls/mr-protocol"; import { parseInternalUrl } from "../../../../internal-urls/parse"; import reviewCustomRequestTemplate from "../../../../prompts/review-custom-request.md" with { type: "text" }; import reviewHeadlessRequestTemplate from "../../../../prompts/review-headless-request.md" with { type: "text" }; import * as gh from "../../../../tools/gh"; import * as glab from "../../../../tools/glab-mr"; import { resolveToCwd } from "../../../../tools/path-utils"; import { listDirectChildRepos } from "../../../../utils/active-repo-context"; import { github } from "../../../../utils/github"; import { gitlab } from "../../../../utils/gitlab"; import { buildReviewPrompt } from "./prompt"; import { createResolvedReviewTarget, getReviewTargetIssue, LOCAL_REVIEW_CHOICES, type LocalReviewKind, type ResolvedReviewTarget, readUncommittedReviewTarget, resolveLocalReviewTarget, reviewRepository, } from "./target"; /** `/review [] [ | #N | !N] [instructions]` after resolution. */ export interface ReviewRequest { /** Leading `` when given, else the session cwd. */ cwd: string; prRef: ReviewPrRef | undefined; extraInstructions: string; } /** A GitHub pull request or GitLab merge request to review. */ export interface ReviewPrRef { forge: "github" | "gitlab"; /** GitHub `[HOST/]OWNER/REPO`, or GitLab `//`. */ repo: string; /** PR number or MR iid. */ number: number; raw: string; kind: "github-url" | "pr-url" | "github-ref" | "gitlab-url" | "mr-url"; } /** A diff the reviewer can target: a detected PR/MR or one local diff kind. */ export type ReviewTargetChoice = | { label: string; kind: "pr"; ref: ReviewPrRef } | { label: string; kind: LocalReviewKind }; export type ReviewChoice = ReviewTargetChoice | { label: string; kind: "custom" }; const REVIEW_CONTEXT_PR_LIMIT = 3; const PICK_PR_CHOICE_LABEL = "4. Review a specific PR/MR"; /** `gh pr list` / `glab mr list` page size. 100 is the per-page maximum of both forges. */ const PR_PICKER_LIMIT = 100; const REPO_SEGMENT_PATTERN = /^[A-Za-z0-9_.-]+$/; const GITHUB_REF_PATTERN = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#([1-9]\d*)$/; /** `#N` (GitHub PR) or `!N` (GitLab MR) in the review checkout's repository. */ const CHANGE_NUMBER_PATTERN = /^([#!])([1-9]\d*)$/; /** `.`, `..`, `~` (alone or followed by a separator), an absolute path, or a drive path. */ const DIR_ARG_PATTERN = /^(?:\.{1,2}(?:[\\/]|$)|~(?:[\\/]|$)|[\\/]|[A-Za-z]:[\\/])/; const PR_SCHEME_PATTERN = /^pr:\/\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/([1-9]\d*)(?:\/diff(?:\/(?:all|[1-9]\d*))?)?(?:\?at=\d+)?$/; const PR_REF_TEXT_PATTERN = /https:\/\/github\.com\/[^\s<>"']+|pr:\/\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[^\s<>"']+|https:\/\/[^\s<>"'/]+\/[^\s<>"']*\/-\/merge_requests\/[^\s<>"']+|mr:\/\/[^\s<>"']+/g; function stripTrailingPrRefPunctuation(text: string): string { return text.replace(/[.,)\]>]+$/g, ""); } function isValidRepoSegment(segment: string | undefined): segment is string { return segment !== undefined && REPO_SEGMENT_PATTERN.test(segment); } function parsePositivePrNumber(value: string | undefined): number | undefined { if (value === undefined || !/^[1-9]\d*$/.test(value)) return undefined; const parsed = Number(value); return Number.isSafeInteger(parsed) ? parsed : undefined; } function parseGithubPrUrl(text: string): ReviewPrRef | undefined { let url: URL; try { url = new URL(text); } catch { return undefined; } if (url.protocol !== "https:" || url.hostname !== "github.com") return undefined; const parts = url.pathname.split("/").filter(Boolean); if (parts.length < 4 || parts[2] !== "pull") return undefined; const [owner, repo, , numberPart] = parts; if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) return undefined; const number = parsePositivePrNumber(numberPart); if (number === undefined) return undefined; return { forge: "github", repo: `${owner}/${repo}`, number, raw: text, kind: "github-url" }; } function parsePrSchemeRef(text: string): ReviewPrRef | undefined { const match = PR_SCHEME_PATTERN.exec(text); if (!match) return undefined; const [, owner, repo, numberPart] = match; const number = parsePositivePrNumber(numberPart); if (number === undefined) return undefined; return { forge: "github", repo: `${owner}/${repo}`, number, raw: text, kind: "pr-url" }; } /** GitHub `owner/repo#N` shorthand. */ function parseGithubRef(text: string): ReviewPrRef | undefined { const match = GITHUB_REF_PATTERN.exec(text); if (!match) return undefined; const [, owner, repo, numberPart] = match; const number = parsePositivePrNumber(numberPart); if (number === undefined) return undefined; return { forge: "github", repo: `${owner}/${repo}`, number, raw: text, kind: "github-ref" }; } /** `https://///-/merge_requests/[/…]` */ function parseGitlabMrUrl(text: string): ReviewPrRef | undefined { let url: URL; try { url = new URL(text); } catch { return undefined; } if (url.protocol !== "https:") return undefined; const parts = url.pathname.split("/").filter(Boolean); const dash = parts.indexOf("-"); if (dash < 0 || parts[dash + 1] !== "merge_requests") return undefined; const repo = glab.parseGitlabProjectWebUrl(`${url.origin}/${parts.slice(0, dash).join("/")}`); const number = parsePositivePrNumber(parts[dash + 2]); if (!repo || number === undefined) return undefined; return { forge: "gitlab", repo, number, raw: text, kind: "gitlab-url" }; } /** Fully qualified `mr://////diff[/…]`; the short `mr:///…` form names no project. */ function parseMrSchemeRef(text: string): ReviewPrRef | undefined { if (!text.startsWith("mr://")) return undefined; try { const parsed = parseMrUrl(parseInternalUrl(text)); if (!parsed.project) return undefined; return { forge: "gitlab", repo: parsed.project, number: parsed.iid, raw: text, kind: "mr-url" }; } catch { return undefined; } } export function parseReviewPrRef(text: string): ReviewPrRef | undefined { const candidate = stripTrailingPrRefPunctuation(text); return ( parseGithubPrUrl(candidate) ?? parsePrSchemeRef(candidate) ?? parseGithubRef(candidate) ?? parseGitlabMrUrl(candidate) ?? parseMrSchemeRef(candidate) ); } /** `PR owner/repo#N` for GitHub, `MR host/group/project!N` for GitLab. */ export function formatReviewRefLabel(ref: ReviewPrRef): string { return ref.forge === "gitlab" ? `MR ${ref.repo}!${ref.number}` : `PR ${ref.repo}#${ref.number}`; } /** * A leading path that is an existing directory names the checkout; any other * leading path (`./src/auth.ts …`) stays instruction text. Only the next * argument may be a `#N`/`!N` number or `owner/repo#N`, so prose citing a * related PR keeps its meaning; full URLs and `pr://`/`mr://` still match * anywhere. Throws when a number cannot be resolved. */ export async function resolveReviewRequest(args: string[], sessionCwd: string): Promise { const rest = [...args]; let cwd = sessionCwd; if (rest[0] !== undefined && DIR_ARG_PATTERN.test(rest[0])) { const dir = resolveToCwd(rest[0], sessionCwd); const stat = await fs.promises.stat(dir).catch(() => undefined); if (stat?.isDirectory()) { cwd = dir; rest.shift(); } } let prRef: ReviewPrRef | undefined; const numberMatch = rest[0] !== undefined ? CHANGE_NUMBER_PATTERN.exec(rest[0]) : null; const number = numberMatch ? parsePositivePrNumber(numberMatch[2]) : undefined; if (numberMatch && number !== undefined) { rest.shift(); try { prRef = numberMatch[1] === "!" ? mrNumberRef(await glab.resolveDefaultGitlabProjectMemoized(cwd), number) : prNumberRef(await gh.resolveDefaultRepoMemoized(cwd), number); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Cannot resolve ${numberMatch[0]} in ${pickerText(shortenPath(cwd))}: ${message}`); } } else { // `owner/repo#N` counts only first; URL and scheme refs count anywhere. const index = rest.findIndex((arg, i) => { const ref = parseReviewPrRef(arg); return ref !== undefined && (i === 0 || ref.kind !== "github-ref"); }); if (index >= 0) prRef = parseReviewPrRef(rest.splice(index, 1)[0]!); } return { cwd, prRef, extraInstructions: rest.join(" ") }; } function extractReviewPrRefsFromText(text: string): ReviewPrRef[] { return Array.from(text.matchAll(PR_REF_TEXT_PATTERN), match => parseReviewPrRef(match[0])).filter( (ref): ref is ReviewPrRef => ref !== undefined, ); } /** Fetch one PR/MR patch and freeze it before any overlay or LLM prompt is built; throws on fetch failure. */ export async function fetchPrReviewTarget(cwd: string, ref: ReviewPrRef): Promise { const label = formatReviewRefLabel(ref); // Session settings are the active scope (set around `/review`). Without them, // `resolveCacheTtl(undefined)` uses the default enabled cache and a kill switch // still stores a generation. const settings = findScopedSettings(cwd); if (ref.forge === "gitlab") { try { const lookup = await glab.getOrFetchMrDiff({ cwd, project: ref.repo, iid: ref.number, ...(settings ? { settings } : {}), }); return createResolvedReviewTarget( "pr", label, lookup.payload.unified, `${label} has no diff content available`, { prDiffUrl: `mr://${ref.repo}/${ref.number}/diff`, }, ); } catch (error) { throw new Error( `Failed to fetch MR diff for ${ref.repo}!${ref.number}: ${error instanceof Error ? error.message : String(error)}`, ); } } try { const lookup = await gh.getOrFetchPrDiff({ cwd, repo: ref.repo, number: ref.number, ...(settings ? { settings } : {}), }); const generationAt = lookup.status === "disabled" ? null : gh.retainPrDiffGeneration({ repo: ref.repo, number: ref.number, rendered: lookup.rendered, files: lookup.payload.files, fetchedAt: lookup.fetchedAt, }); return createResolvedReviewTarget("pr", label, lookup.payload.unified, `${label} has no diff content available`, { prDiffUrl: `pr://${ref.repo}/${ref.number}/diff`, prDiffQuery: generationAt !== null ? `?at=${generationAt}` : "", }); } catch (error) { throw new Error( `Failed to fetch PR diff for ${ref.repo}#${ref.number}: ${error instanceof Error ? error.message : String(error)}`, ); } } /** Interactive wrapper: notifies instead of throwing when a UI is available. */ export async function resolvePrReviewTarget( cwd: string, ctx: HookCommandContext, ref: ReviewPrRef, ): Promise { try { return await fetchPrReviewTarget(cwd, ref); } catch (error) { if (!ctx.hasUI) throw error; ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); return undefined; } } function getTextContentParts(content: unknown): string[] { if (typeof content === "string") return [content]; if (!Array.isArray(content)) return []; const parts: string[] = []; for (const item of content) { if (isRecord(item) && item.type === "text" && typeof item.text === "string") parts.push(item.text); } return parts; } export function findRecentPrRefs(ctx: HookCommandContext, limit: number): ReviewPrRef[] { const refs: ReviewPrRef[] = []; const seen = new Set(); const entries = ctx.sessionManager.getBranch(); for (let index = entries.length - 1; index >= 0 && refs.length < limit; index--) { const entry = entries[index]; if (entry?.type !== "message") continue; const message = entry.message; if (message.role !== "user" && message.role !== "assistant") continue; const parts = getTextContentParts(message.content); for (let partIndex = parts.length - 1; partIndex >= 0 && refs.length < limit; partIndex--) { const partRefs = extractReviewPrRefsFromText(parts[partIndex]!); for (let refIndex = partRefs.length - 1; refIndex >= 0 && refs.length < limit; refIndex--) { const ref = partRefs[refIndex]!; const key = `${ref.forge}:${ref.repo.toLowerCase()}#${ref.number}`; if (seen.has(key)) continue; seen.add(key); refs.push(ref); } } } return refs; } interface PrListItem { number?: number; title?: string; author?: { login?: string } | null; isDraft?: boolean; url?: string; } /** One row of `glab mr list -F json` (GitLab REST list-MRs shape). */ interface GitlabMrListItem { iid?: number; title?: string; author?: { username?: string } | null; draft?: boolean; web_url?: string; } type PickablePr = PrListItem & { number: number }; /** One line, width-bounded: control characters and tabs never reach the picker. */ function pickerText(text: string): string { const collapsed = text.replace(/[\r\n\x00-\x1F\x7F]+/g, " "); return truncateToWidth(replaceTabs(collapsed).replace(/\s+/g, " ").trim(), TRUNCATE_LENGTHS.TITLE); } function formatPrPickerLabel(item: PickablePr, prefix: string): string { const title = pickerText(item.title ?? "") || "(no title)"; const author = item.author?.login?.trim() || "unknown"; const draftSuffix = item.isDraft ? " [draft]" : ""; return `${prefix}${item.number} ${title} @${author}${draftSuffix}`; } function prNumberRef(repo: string, number: number): ReviewPrRef { return { forge: "github", repo, number, raw: `pr://${repo}/${number}`, kind: "pr-url" }; } function mrNumberRef(project: string, iid: number): ReviewPrRef { return { forge: "gitlab", repo: project, number: iid, raw: `mr://${project}/${iid}`, kind: "mr-url" }; } function pickablePrs(items: PrListItem[]): PickablePr[] { return items.filter( (item): item is PickablePr => typeof item.number === "number" && Number.isSafeInteger(item.number) && item.number > 0, ); } /** The forge-specific half of the open PR/MR picker. */ interface ChangeRequestSource { repo: string; plural: "pull requests" | "merge requests"; prefix: "#" | "!"; searchPrompt: string; /** One page of open items, optionally narrowed by a server-side search; throws on CLI failure. */ fetch(search: string | undefined): Promise; refFor(item: PickablePr): ReviewPrRef; refForNumber(number: number): ReviewPrRef; } /** * GitHub first; GitLab only when `gh` cannot resolve a repository and `glab` * is installed, so a checkout with both remotes keeps today's GitHub picker. */ async function resolveChangeRequestSource( cwd: string, ctx: HookCommandContext, ): Promise { let ghMessage: string; try { const repo = await gh.resolveDefaultRepoMemoized(cwd); return { repo, plural: "pull requests", prefix: "#", searchPrompt: "Search open pull requests — title, author, or #123", fetch: search => { const args = ["pr", "list", "--repo", repo, "--state", "open", "--limit", String(PR_PICKER_LIMIT)]; args.push("--json", "number,title,author,isDraft,url"); if (search !== undefined) args.push("--search", search); return github.json(cwd, args, undefined, { repoProvided: true }); }, refFor: item => (item.url ? parseGithubPrUrl(item.url) : undefined) ?? prNumberRef(repo, item.number), refForNumber: number => prNumberRef(repo, number), }; } catch (error) { ghMessage = error instanceof Error ? error.message : String(error); } if (!gitlab.available()) { ctx.ui.notify(`Failed to resolve repository for PR picker: ${ghMessage}`, "error"); return undefined; } try { const project = await glab.resolveDefaultGitlabProjectMemoized(cwd); return { repo: project, plural: "merge requests", prefix: "!", searchPrompt: "Search open merge requests — title, description, or !123", fetch: async search => { const args = ["mr", "list", "-R", glab.formatGitlabProjectArg(project), "-F", "json"]; args.push("-P", String(PR_PICKER_LIMIT)); if (search !== undefined) args.push("--search", search); const items = await gitlab.json(cwd, args); return items.map(item => ({ number: item.iid, title: item.title, author: { login: item.author?.username }, isDraft: item.draft, url: item.web_url, })); }, refFor: item => mrNumberRef(project, item.number), refForNumber: iid => mrNumberRef(project, iid), }; } catch (error) { const glMessage = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Failed to resolve repository for PR picker: GitHub: ${ghMessage}; GitLab: ${glMessage}`, "error"); return undefined; } } /** * List the repository's open PRs/MRs with a server-side search row and a * `#123`/`!123` fast-path. Local type-to-filter only covers the fetched page; * the search row reaches items beyond it, and a number skips listing entirely * because the forge list search never matches item numbers. */ async function selectPullRequestRef(cwd: string, ctx: HookCommandContext): Promise { const source = await resolveChangeRequestSource(cwd, ctx); if (!source) return undefined; const { repo, plural, prefix } = source; const searchEntry = `Search open ${plural}…`; /** Notifies and resolves `undefined` on failure. */ const listOpen = async (search: string | undefined): Promise => { ctx.ui.setStatus("review", `Loading open ${plural}…`); try { return pickablePrs(await source.fetch(search)); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Failed to list open ${plural} in ${repo}: ${message}`, "error"); return undefined; } finally { ctx.ui.setStatus("review", undefined); } }; const openPrs = await listOpen(undefined); if (openPrs === undefined) return undefined; if (openPrs.length === 0) { ctx.ui.notify(`No open ${plural} in ${repo}`, "warning"); return undefined; } let activeQuery: string | undefined; let currentPrs = openPrs; for (;;) { const rows = currentPrs.map(item => ({ label: formatPrPickerLabel(item, prefix), item })); const title = activeQuery === undefined ? `Open ${plural} in ${repo}` : `Open ${plural} in ${repo} matching "${activeQuery}"`; const selected = await ctx.ui.select(title, [...rows.map(row => row.label), searchEntry]); if (selected === undefined) return undefined; if (selected !== searchEntry) { const match = rows.find(row => row.label === selected); if (!match) return undefined; return source.refFor(match.item); } // The hint rides in the title: `HookInputComponent` accepts a placeholder // and never renders it, so a second argument would be invisible. const input = await ctx.ui.input(source.searchPrompt); if (input === undefined) continue; const query = input.trim(); if (query === "") { activeQuery = undefined; currentPrs = openPrs; continue; } const number = parsePositivePrNumber(query.startsWith(prefix) ? query.slice(1) : query); if (number !== undefined) return source.refForNumber(number); const matches = await listOpen(query); // The failure was already reported; stay in the picker on the list shown before. if (matches === undefined) continue; if (matches.length === 0) { ctx.ui.notify(`No open ${plural} matching "${query}" in ${repo}`, "warning"); activeQuery = undefined; currentPrs = openPrs; continue; } activeQuery = query; currentPrs = matches; } } type ReviewMenuChoice = ReviewChoice | { label: string; kind: "pick-pr" }; /** A menu pick plus the checkout it reads. */ export interface ReviewSelection { choice: Choice; cwd: string; } /** * `cwd` when it lies in a repository; otherwise a picked direct-child * checkout (the same scan as the active-repo context), so `/review` from a * folder of checkouts still reaches one. */ async function selectCheckout(ctx: HookCommandContext, cwd: string): Promise { if (vcs.repo(cwd)) return cwd; const repos = await listDirectChildRepos(cwd); const where = pickerText(shortenPath(cwd)); if (repos.length === 0) { ctx.ui.notify(`No repository in ${where}; use /review or /review owner/repo#N`, "error"); return undefined; } // Numbered rows stay distinct even when names sanitize or truncate alike. const labels = repos.map((repo, index) => `${index + 1}. ${pickerText(path.basename(repo))}`); const picked = await ctx.ui.select(`Select repository in ${where}`, labels); const index = picked === undefined ? -1 : labels.indexOf(picked); return index < 0 ? undefined : repos[index]; } /** * Review target menu shared by `/review` and `/annotate code-review`. Picking * "Review a specific PR/MR" opens the open-PR/MR picker and resolves to a `pr` * choice, so callers never see the picker step. Local and picker choices * outside a repository first pick a child checkout. */ export async function selectReviewChoice( ctx: HookCommandContext, cwd: string, ): Promise | undefined>; export async function selectReviewChoice( ctx: HookCommandContext, cwd: string, options: { includeCustom: boolean }, ): Promise; export async function selectReviewChoice( ctx: HookCommandContext, cwd: string, options: { includeCustom: boolean } = { includeCustom: false }, ): Promise { const choices: ReviewMenuChoice[] = [ ...findRecentPrRefs(ctx, REVIEW_CONTEXT_PR_LIMIT).map(ref => ({ label: `Review ${formatReviewRefLabel(ref)} from conversation`, kind: "pr" as const, ref, })), ...LOCAL_REVIEW_CHOICES.map(choice => ({ label: choice.label, kind: choice.kind })), { label: PICK_PR_CHOICE_LABEL, kind: "pick-pr" }, ]; if (options.includeCustom) choices.push({ label: "5. Custom review instructions", kind: "custom" }); const selected = await ctx.ui.select( "Review Mode", choices.map(choice => choice.label), ); const choice = choices.find(candidate => candidate.label === selected); if (!choice) return undefined; if (choice.kind === "pr" || choice.kind === "custom") return { choice, cwd }; const checkout = await selectCheckout(ctx, cwd); if (!checkout) return undefined; if (choice.kind !== "pick-pr") return { choice, cwd: checkout }; const ref = await selectPullRequestRef(checkout, ctx); return ref ? { choice: { label: `Review ${formatReviewRefLabel(ref)}`, kind: "pr", ref }, cwd: checkout } : undefined; } function reviewTargetPrompt( ctx: HookCommandContext, target: ResolvedReviewTarget, instructions?: string, ): string | undefined { const issue = getReviewTargetIssue(target); if (issue) { if (ctx.hasUI) ctx.ui.notify(issue, "warning"); return undefined; } return buildReviewPrompt(target, instructions); } function buildHeadlessReviewPrompt(focus: string | undefined, repository: string | undefined): string { return prompt.render(reviewHeadlessRequestTemplate, { focus, repository }); } /** * `api.cwd` freezes at command-load time; after /move or /wt the live session * cwd comes from the session manager (issue #12501). */ export function liveCommandCwd(api: CustomCommandAPI, ctx: HookCommandContext): string { return ctx.sessionManager?.getCwd?.() || api.cwd; } function buildCustomReviewPrompt(instructions: string, repository: string | undefined): string { return prompt.render(reviewCustomRequestTemplate, { instructions, repository }); } export class ReviewCommand implements CustomCommand { name = "review"; description = "Launch interactive code review"; constructor(private readonly api: CustomCommandAPI) {} async execute(args: string[], ctx: HookCommandContext): Promise { const sessionCwd = liveCommandCwd(this.api, ctx); let request: ReviewRequest; try { request = await resolveReviewRequest(args, sessionCwd); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!ctx.hasUI) return message; ctx.ui.notify(message, "error"); return undefined; } const { prRef } = request; const extraInstructions = request.extraInstructions || undefined; if (prRef) { try { const target = await resolvePrReviewTarget(request.cwd, ctx, prRef); const result = target ? reviewTargetPrompt(ctx, target, extraInstructions) : undefined; return ( result ?? (ctx.hasUI ? undefined : `Unable to review ${formatReviewRefLabel(prRef)}: no diff content available.`) ); } catch (error) { return error instanceof Error ? error.message : String(error); } } if (!ctx.hasUI) { return buildHeadlessReviewPrompt(extraInstructions, reviewRepository(request.cwd, sessionCwd)); } const selection = await selectReviewChoice(ctx, request.cwd, { includeCustom: !extraInstructions }); if (!selection) return undefined; const { choice, cwd } = selection; const repository = reviewRepository(cwd, sessionCwd); if (choice.kind === "pr") { const target = await resolvePrReviewTarget(cwd, ctx, choice.ref); return target ? reviewTargetPrompt(ctx, target, extraInstructions) : undefined; } if (choice.kind === "custom") { const instructions = await ctx.ui.editor( "Enter custom review instructions", "Review the following:\n\n", undefined, { promptStyle: true }, ); if (!instructions?.trim()) return undefined; const target = await readUncommittedReviewTarget(cwd).catch(() => undefined); if (target?.rawDiff.trim()) { return buildReviewPrompt( { ...target, mode: `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`, repository }, instructions, ); } return buildCustomReviewPrompt(instructions, repository); } const target = await resolveLocalReviewTarget(choice.kind, cwd, ctx.ui); return target ? reviewTargetPrompt(ctx, { ...target, repository }, extraInstructions) : undefined; } } export default ReviewCommand;