|
|
|
@@ -15,6 +15,16 @@ import path from "node:path"
|
|
|
|
type BookmarkRef = { name: string; remote?: string }
|
|
|
|
type BookmarkRef = { name: string; remote?: string }
|
|
|
|
type Change = { changeId: string; title: string }
|
|
|
|
type Change = { changeId: string; title: string }
|
|
|
|
type JjRemote = { name: string; url: string }
|
|
|
|
type JjRemote = { name: string; url: string }
|
|
|
|
|
|
|
|
type PullRequestListItem = {
|
|
|
|
|
|
|
|
prNumber: number
|
|
|
|
|
|
|
|
title: string
|
|
|
|
|
|
|
|
updatedAt: string
|
|
|
|
|
|
|
|
reviewRequested: boolean
|
|
|
|
|
|
|
|
author?: string
|
|
|
|
|
|
|
|
baseRefName?: string
|
|
|
|
|
|
|
|
headRefName?: string
|
|
|
|
|
|
|
|
isManualEntry?: boolean
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ReviewTarget =
|
|
|
|
type ReviewTarget =
|
|
|
|
| { type: "workingCopy" }
|
|
|
|
| { type: "workingCopy" }
|
|
|
|
@@ -32,6 +42,9 @@ type ReviewTarget =
|
|
|
|
type ReviewSelectorValue = ReviewTarget["type"] | "toggleCustomInstructions"
|
|
|
|
type ReviewSelectorValue = ReviewTarget["type"] | "toggleCustomInstructions"
|
|
|
|
|
|
|
|
|
|
|
|
const CUSTOM_INSTRUCTIONS_KEY = "review.customInstructions"
|
|
|
|
const CUSTOM_INSTRUCTIONS_KEY = "review.customInstructions"
|
|
|
|
|
|
|
|
const MIN_CHANGE_REVIEW_OPTIONS = 10
|
|
|
|
|
|
|
|
const RECENT_PULL_REQUEST_LIMIT = 5
|
|
|
|
|
|
|
|
const PULL_REQUEST_MAX_AGE_DAYS = 7
|
|
|
|
|
|
|
|
|
|
|
|
const WORKING_COPY_PROMPT =
|
|
|
|
const WORKING_COPY_PROMPT =
|
|
|
|
"Review the current working-copy changes (including new files) and provide prioritized findings."
|
|
|
|
"Review the current working-copy changes (including new files) and provide prioritized findings."
|
|
|
|
@@ -163,6 +176,13 @@ Provide your findings in a clear, structured format:
|
|
|
|
|
|
|
|
|
|
|
|
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue. Then append the required non-blocking callouts section.`
|
|
|
|
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue. Then append the required non-blocking callouts section.`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeCustomInstructions(
|
|
|
|
|
|
|
|
value: string | undefined,
|
|
|
|
|
|
|
|
): string | undefined {
|
|
|
|
|
|
|
|
const normalized = value?.trim()
|
|
|
|
|
|
|
|
return normalized ? normalized : undefined
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function bookmarkLabel(b: BookmarkRef): string {
|
|
|
|
function bookmarkLabel(b: BookmarkRef): string {
|
|
|
|
return b.remote ? `${b.name}@${b.remote}` : b.name
|
|
|
|
return b.remote ? `${b.name}@${b.remote}` : b.name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -228,6 +248,115 @@ function parsePrRef(ref: string): number | null {
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function formatRelativeTime(value: string): string | null {
|
|
|
|
|
|
|
|
const timestamp = Date.parse(value)
|
|
|
|
|
|
|
|
if (!Number.isFinite(timestamp)) return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const deltaMs = Date.now() - timestamp
|
|
|
|
|
|
|
|
const future = deltaMs < 0
|
|
|
|
|
|
|
|
const absoluteSeconds = Math.round(Math.abs(deltaMs) / 1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (absoluteSeconds < 60) return future ? "in <1m" : "just now"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const units = [
|
|
|
|
|
|
|
|
{ label: "y", seconds: 60 * 60 * 24 * 365 },
|
|
|
|
|
|
|
|
{ label: "mo", seconds: 60 * 60 * 24 * 30 },
|
|
|
|
|
|
|
|
{ label: "d", seconds: 60 * 60 * 24 },
|
|
|
|
|
|
|
|
{ label: "h", seconds: 60 * 60 },
|
|
|
|
|
|
|
|
{ label: "m", seconds: 60 },
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const unit of units) {
|
|
|
|
|
|
|
|
if (absoluteSeconds >= unit.seconds) {
|
|
|
|
|
|
|
|
const count = Math.floor(absoluteSeconds / unit.seconds)
|
|
|
|
|
|
|
|
return future ? `in ${count}${unit.label}` : `${count}${unit.label} ago`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return future ? "soon" : "just now"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getPullRequestUpdatedSinceDate(days: number): string {
|
|
|
|
|
|
|
|
const timestamp = Date.now() - days * 24 * 60 * 60 * 1000
|
|
|
|
|
|
|
|
return new Date(timestamp).toISOString().slice(0, 10)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parsePullRequests(stdout: string): PullRequestListItem[] {
|
|
|
|
|
|
|
|
let parsed: unknown
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
parsed = JSON.parse(stdout)
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(parsed)) return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return parsed.flatMap((entry) => {
|
|
|
|
|
|
|
|
if (!entry || typeof entry !== "object") return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const prNumber =
|
|
|
|
|
|
|
|
typeof entry.number === "number" && Number.isSafeInteger(entry.number)
|
|
|
|
|
|
|
|
? entry.number
|
|
|
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
const title = typeof entry.title === "string" ? entry.title.trim() : ""
|
|
|
|
|
|
|
|
const updatedAt =
|
|
|
|
|
|
|
|
typeof entry.updatedAt === "string" ? entry.updatedAt.trim() : ""
|
|
|
|
|
|
|
|
if (!prNumber || !title || !updatedAt) return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
prNumber,
|
|
|
|
|
|
|
|
title,
|
|
|
|
|
|
|
|
updatedAt,
|
|
|
|
|
|
|
|
reviewRequested: entry.reviewRequested === true,
|
|
|
|
|
|
|
|
author:
|
|
|
|
|
|
|
|
entry.author &&
|
|
|
|
|
|
|
|
typeof entry.author === "object" &&
|
|
|
|
|
|
|
|
typeof entry.author.login === "string"
|
|
|
|
|
|
|
|
? entry.author.login
|
|
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
baseRefName:
|
|
|
|
|
|
|
|
typeof entry.baseRefName === "string"
|
|
|
|
|
|
|
|
? entry.baseRefName.trim() || undefined
|
|
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
headRefName:
|
|
|
|
|
|
|
|
typeof entry.headRefName === "string"
|
|
|
|
|
|
|
|
? entry.headRefName.trim() || undefined
|
|
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function dedupePullRequests(
|
|
|
|
|
|
|
|
pullRequests: PullRequestListItem[],
|
|
|
|
|
|
|
|
): PullRequestListItem[] {
|
|
|
|
|
|
|
|
const seen = new Set<number>()
|
|
|
|
|
|
|
|
const result: PullRequestListItem[] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const pullRequest of pullRequests) {
|
|
|
|
|
|
|
|
if (seen.has(pullRequest.prNumber)) continue
|
|
|
|
|
|
|
|
seen.add(pullRequest.prNumber)
|
|
|
|
|
|
|
|
result.push(pullRequest)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildPullRequestOptionDescription(pr: PullRequestListItem): string {
|
|
|
|
|
|
|
|
if (pr.isManualEntry) return "(enter PR number or URL)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const parts = [pr.reviewRequested ? "review requested" : "recent"]
|
|
|
|
|
|
|
|
const relativeTime = formatRelativeTime(pr.updatedAt)
|
|
|
|
|
|
|
|
if (relativeTime) parts.push(`updated ${relativeTime}`)
|
|
|
|
|
|
|
|
if (pr.author) parts.push(`@${pr.author}`)
|
|
|
|
|
|
|
|
if (pr.baseRefName && pr.headRefName) {
|
|
|
|
|
|
|
|
parts.push(`${pr.headRefName} → ${pr.baseRefName}`)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return `(${parts.join(" · ")})`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeRemoteUrl(value: string): string {
|
|
|
|
function normalizeRemoteUrl(value: string): string {
|
|
|
|
return value
|
|
|
|
return value
|
|
|
|
.trim()
|
|
|
|
.trim()
|
|
|
|
@@ -254,14 +383,6 @@ function getRepositoryUrl(value: string): string | null {
|
|
|
|
|
|
|
|
|
|
|
|
const plugin: TuiPlugin = async (api) => {
|
|
|
|
const plugin: TuiPlugin = async (api) => {
|
|
|
|
const cwd = api.state.path.directory
|
|
|
|
const cwd = api.state.path.directory
|
|
|
|
let reviewCustomInstructions = normalizeCustomInstructions(
|
|
|
|
|
|
|
|
api.kv.get<string | undefined>(CUSTOM_INSTRUCTIONS_KEY, undefined),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setReviewCustomInstructions(value?: string): void {
|
|
|
|
|
|
|
|
reviewCustomInstructions = normalizeCustomInstructions(value)
|
|
|
|
|
|
|
|
api.kv.set(CUSTOM_INSTRUCTIONS_KEY, reviewCustomInstructions)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function exec(
|
|
|
|
async function exec(
|
|
|
|
cmd: string,
|
|
|
|
cmd: string,
|
|
|
|
@@ -302,16 +423,29 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
return (await jj("root")).ok
|
|
|
|
return (await jj("root")).ok
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const reviewCustomInstructionsKey = `${CUSTOM_INSTRUCTIONS_KEY}-${cwd}`
|
|
|
|
|
|
|
|
let reviewCustomInstructions = normalizeCustomInstructions(
|
|
|
|
|
|
|
|
api.kv.get<string | undefined>(reviewCustomInstructionsKey, undefined),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setReviewCustomInstructions(value?: string): void {
|
|
|
|
|
|
|
|
reviewCustomInstructions = normalizeCustomInstructions(value)
|
|
|
|
|
|
|
|
api.kv.set(reviewCustomInstructionsKey, reviewCustomInstructions)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function hasWorkingCopyChanges(): Promise<boolean> {
|
|
|
|
async function hasWorkingCopyChanges(): Promise<boolean> {
|
|
|
|
const r = await jj("diff", "--summary")
|
|
|
|
const r = await jj("diff", "--summary")
|
|
|
|
return r.ok && r.stdout.trim().length > 0
|
|
|
|
return r.ok && r.stdout.trim().length > 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getRecentChanges(limit = 20): Promise<Change[]> {
|
|
|
|
async function getRecentChanges(limit = 20): Promise<Change[]> {
|
|
|
|
|
|
|
|
const effectiveLimit = Math.max(limit, MIN_CHANGE_REVIEW_OPTIONS)
|
|
|
|
const r = await jj(
|
|
|
|
const r = await jj(
|
|
|
|
"log",
|
|
|
|
"log",
|
|
|
|
|
|
|
|
"-r",
|
|
|
|
|
|
|
|
"all()",
|
|
|
|
"-n",
|
|
|
|
"-n",
|
|
|
|
String(limit),
|
|
|
|
String(effectiveLimit),
|
|
|
|
"--no-graph",
|
|
|
|
"--no-graph",
|
|
|
|
"-T",
|
|
|
|
"-T",
|
|
|
|
'change_id.shortest(8) ++ "\\t" ++ description.first_line() ++ "\\n"',
|
|
|
|
'change_id.shortest(8) ++ "\\t" ++ description.first_line() ++ "\\n"',
|
|
|
|
@@ -320,6 +454,53 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
return parseChanges(r.stdout)
|
|
|
|
return parseChanges(r.stdout)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function getPullRequests(
|
|
|
|
|
|
|
|
args: string[],
|
|
|
|
|
|
|
|
reviewRequested: boolean,
|
|
|
|
|
|
|
|
): Promise<PullRequestListItem[]> {
|
|
|
|
|
|
|
|
const response = await gh(
|
|
|
|
|
|
|
|
"pr",
|
|
|
|
|
|
|
|
"list",
|
|
|
|
|
|
|
|
...args,
|
|
|
|
|
|
|
|
"--json",
|
|
|
|
|
|
|
|
"number,title,updatedAt,author,baseRefName,headRefName",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!response.ok) return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return parsePullRequests(response.stdout).map((pr) => ({
|
|
|
|
|
|
|
|
...pr,
|
|
|
|
|
|
|
|
reviewRequested,
|
|
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function getSelectablePullRequests(): Promise<PullRequestListItem[]> {
|
|
|
|
|
|
|
|
const updatedSince = getPullRequestUpdatedSinceDate(
|
|
|
|
|
|
|
|
PULL_REQUEST_MAX_AGE_DAYS,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
const [reviewRequested, recent] = await Promise.all([
|
|
|
|
|
|
|
|
getPullRequests(
|
|
|
|
|
|
|
|
[
|
|
|
|
|
|
|
|
"--search",
|
|
|
|
|
|
|
|
`review-requested:@me updated:>=${updatedSince} sort:updated-desc`,
|
|
|
|
|
|
|
|
"--limit",
|
|
|
|
|
|
|
|
"50",
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
true,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
getPullRequests(
|
|
|
|
|
|
|
|
[
|
|
|
|
|
|
|
|
"--search",
|
|
|
|
|
|
|
|
`updated:>=${updatedSince} sort:updated-desc`,
|
|
|
|
|
|
|
|
"--limit",
|
|
|
|
|
|
|
|
String(RECENT_PULL_REQUEST_LIMIT),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
false,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return dedupePullRequests([...reviewRequested, ...recent])
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function getMergeBase(
|
|
|
|
async function getMergeBase(
|
|
|
|
bookmark: string,
|
|
|
|
bookmark: string,
|
|
|
|
remote?: string,
|
|
|
|
remote?: string,
|
|
|
|
@@ -479,13 +660,6 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeCustomInstructions(
|
|
|
|
|
|
|
|
value: string | undefined,
|
|
|
|
|
|
|
|
): string | undefined {
|
|
|
|
|
|
|
|
const normalized = value?.trim()
|
|
|
|
|
|
|
|
return normalized ? normalized : undefined
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseNonEmptyLines(stdout: string): string[] {
|
|
|
|
function parseNonEmptyLines(stdout: string): string[] {
|
|
|
|
return stdout
|
|
|
|
return stdout
|
|
|
|
.trim()
|
|
|
|
.trim()
|
|
|
|
@@ -781,13 +955,10 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
target.type !== "workingCopy" && (await hasWorkingCopyChanges()),
|
|
|
|
target.type !== "workingCopy" && (await hasWorkingCopyChanges()),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
const projectGuidelines = await loadProjectReviewGuidelines()
|
|
|
|
const projectGuidelines = await loadProjectReviewGuidelines()
|
|
|
|
const sharedInstructions = normalizeCustomInstructions(
|
|
|
|
|
|
|
|
reviewCustomInstructions,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`
|
|
|
|
let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`
|
|
|
|
|
|
|
|
|
|
|
|
if (sharedInstructions) {
|
|
|
|
if (reviewCustomInstructions) {
|
|
|
|
fullPrompt += `\n\nShared custom review instructions (applies to all reviews):\n\n${sharedInstructions}`
|
|
|
|
fullPrompt += `\n\nCustom review instructions for this working directory (applies to all review modes here):\n\n${reviewCustomInstructions}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (projectGuidelines) {
|
|
|
|
if (projectGuidelines) {
|
|
|
|
@@ -906,8 +1077,8 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
: "Add custom review instructions",
|
|
|
|
: "Add custom review instructions",
|
|
|
|
value: "toggleCustomInstructions",
|
|
|
|
value: "toggleCustomInstructions",
|
|
|
|
description: reviewCustomInstructions
|
|
|
|
description: reviewCustomInstructions
|
|
|
|
? "(currently set)"
|
|
|
|
? "(set for this directory)"
|
|
|
|
: "(applies to all review modes)",
|
|
|
|
: "(this directory, all review modes)",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
@@ -930,7 +1101,7 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
void showChangeSelector()
|
|
|
|
void showChangeSelector()
|
|
|
|
break
|
|
|
|
break
|
|
|
|
case "pullRequest":
|
|
|
|
case "pullRequest":
|
|
|
|
void showPrInput()
|
|
|
|
void showPrSelector()
|
|
|
|
break
|
|
|
|
break
|
|
|
|
case "folder":
|
|
|
|
case "folder":
|
|
|
|
showFolderInput()
|
|
|
|
showFolderInput()
|
|
|
|
@@ -939,7 +1110,8 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
if (reviewCustomInstructions) {
|
|
|
|
if (reviewCustomInstructions) {
|
|
|
|
setReviewCustomInstructions(undefined)
|
|
|
|
setReviewCustomInstructions(undefined)
|
|
|
|
api.ui.toast({
|
|
|
|
api.ui.toast({
|
|
|
|
message: "Custom review instructions removed",
|
|
|
|
message:
|
|
|
|
|
|
|
|
"Custom review instructions removed for this directory",
|
|
|
|
variant: "info",
|
|
|
|
variant: "info",
|
|
|
|
})
|
|
|
|
})
|
|
|
|
void showReviewSelector()
|
|
|
|
void showReviewSelector()
|
|
|
|
@@ -974,7 +1146,8 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
|
|
|
|
|
|
|
|
setReviewCustomInstructions(next)
|
|
|
|
setReviewCustomInstructions(next)
|
|
|
|
api.ui.toast({
|
|
|
|
api.ui.toast({
|
|
|
|
message: "Custom review instructions saved",
|
|
|
|
message:
|
|
|
|
|
|
|
|
"Custom review instructions saved for this directory",
|
|
|
|
variant: "success",
|
|
|
|
variant: "success",
|
|
|
|
})
|
|
|
|
})
|
|
|
|
void showReviewSelector()
|
|
|
|
void showReviewSelector()
|
|
|
|
@@ -1084,16 +1257,7 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function showPrInput(): Promise<void> {
|
|
|
|
function showPrManualInput(): void {
|
|
|
|
if (await hasWorkingCopyChanges()) {
|
|
|
|
|
|
|
|
api.ui.toast({
|
|
|
|
|
|
|
|
message:
|
|
|
|
|
|
|
|
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
|
|
|
|
|
|
|
|
variant: "error",
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api.ui.dialog.replace(
|
|
|
|
api.ui.dialog.replace(
|
|
|
|
() =>
|
|
|
|
() =>
|
|
|
|
api.ui.DialogPrompt({
|
|
|
|
api.ui.DialogPrompt({
|
|
|
|
@@ -1113,6 +1277,69 @@ const plugin: TuiPlugin = async (api) => {
|
|
|
|
api.ui.dialog.clear()
|
|
|
|
api.ui.dialog.clear()
|
|
|
|
void handlePrReview(prNumber)
|
|
|
|
void handlePrReview(prNumber)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
|
|
|
api.ui.dialog.clear()
|
|
|
|
|
|
|
|
void showPrSelector()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function showPrSelector(): Promise<void> {
|
|
|
|
|
|
|
|
if (await hasWorkingCopyChanges()) {
|
|
|
|
|
|
|
|
api.ui.toast({
|
|
|
|
|
|
|
|
message:
|
|
|
|
|
|
|
|
"Cannot materialize PR: you have local jj changes. Please snapshot or discard them first.",
|
|
|
|
|
|
|
|
variant: "error",
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api.ui.toast({ message: "Loading pull requests...", variant: "info" })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pullRequests = await getSelectablePullRequests()
|
|
|
|
|
|
|
|
const options: TuiDialogSelectOption<PullRequestListItem>[] = [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
title: "Enter a PR number or URL",
|
|
|
|
|
|
|
|
value: {
|
|
|
|
|
|
|
|
prNumber: -1,
|
|
|
|
|
|
|
|
title: "Manual entry",
|
|
|
|
|
|
|
|
updatedAt: "",
|
|
|
|
|
|
|
|
reviewRequested: false,
|
|
|
|
|
|
|
|
isManualEntry: true,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
description: "(override the list)",
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
...pullRequests.map((pr) => ({
|
|
|
|
|
|
|
|
title: `#${pr.prNumber} ${pr.title}`,
|
|
|
|
|
|
|
|
value: pr,
|
|
|
|
|
|
|
|
description: buildPullRequestOptionDescription(pr),
|
|
|
|
|
|
|
|
})),
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pullRequests.length === 0) {
|
|
|
|
|
|
|
|
api.ui.toast({
|
|
|
|
|
|
|
|
message:
|
|
|
|
|
|
|
|
"No pull requests found from GitHub; you can still enter a PR number or URL.",
|
|
|
|
|
|
|
|
variant: "info",
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api.ui.dialog.replace(
|
|
|
|
|
|
|
|
() =>
|
|
|
|
|
|
|
|
api.ui.DialogSelect({
|
|
|
|
|
|
|
|
title: "Select pull request to review",
|
|
|
|
|
|
|
|
placeholder: "Filter pull requests...",
|
|
|
|
|
|
|
|
options,
|
|
|
|
|
|
|
|
onSelect: (option) => {
|
|
|
|
|
|
|
|
api.ui.dialog.clear()
|
|
|
|
|
|
|
|
if (option.value.isManualEntry) {
|
|
|
|
|
|
|
|
showPrManualInput()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void handlePrReview(option.value.prNumber)
|
|
|
|
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|