feat(cli): ask for local-review base

This commit is contained in:
Alex Alecu
2026-05-06 12:19:20 +03:00
parent 68b2725f22
commit 838670adbe
5 changed files with 142 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Ask which base branch `/local-review` should review against before starting the review.
@@ -0,0 +1,33 @@
import { Question } from "@/question"
import { SessionID } from "@/session/schema"
import { Review } from "./review"
export namespace ReviewBranch {
export async function resolve(input: { sessionID: SessionID }) {
const base = await Review.getBaseBranch()
const answers = await Question.ask({
sessionID: input.sessionID,
blocking: true,
questions: [
{
header: "Base branch",
question: "Which base branch should I review against?",
custom: true,
options: [
{
label: base,
description: "Review the current branch against this base branch",
},
],
},
],
})
const answer = answers[0]?.[0]?.trim()
return answer || base
}
export async function template(input: { sessionID: SessionID }) {
const base = await resolve(input)
return Review.buildReviewPromptBranch(base)
}
}
@@ -280,8 +280,8 @@ export namespace Review {
*
* @returns Complete prompt string ready for LLM
*/
export async function buildReviewPromptBranch(): Promise<string> {
const base = await getBaseBranch()
export async function buildReviewPromptBranch(baseBranch?: string): Promise<string> {
const base = baseBranch ?? (await getBaseBranch())
const currentBranch = await getCurrentBranch()
const diff = await getBranchChanges(base)
+9 -1
View File
@@ -8,6 +8,7 @@ import { KiloCostPropagation } from "@/kilocode/session/cost-propagation" // kil
import { KiloSessionProcessor } from "@/kilocode/session/processor" // kilocode_change
import { Suggestion } from "@/kilocode/suggestion" // kilocode_change
import { Question } from "@/question" // kilocode_change
import { ReviewBranch } from "@/kilocode/review/base" // kilocode_change
import z from "zod"
import * as EffectZod from "@/util/effect-zod"
import { SessionID, MessageID, PartID } from "./schema"
@@ -1711,7 +1712,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
const templateCommand = yield* Effect.promise(async () => cmd.template)
const templateCommand = yield* Effect.gen(function* () {
// kilocode_change start - ask for the /local-review base branch before building the prompt
if (input.command === Command.Default.LOCAL_REVIEW && cmd.source === undefined) {
return yield* Effect.promise(() => ReviewBranch.template({ sessionID: input.sessionID }))
}
// kilocode_change end
return yield* Effect.promise(async () => cmd.template)
})
const placeholders = templateCommand.match(placeholderRegex) ?? []
let last = 0
@@ -0,0 +1,93 @@
import { $ } from "bun"
import { describe, expect, test } from "bun:test"
import path from "path"
import { Effect } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { Instance } from "../../src/project/instance"
import { Question } from "../../src/question"
import { ReviewBranch } from "../../src/kilocode/review/base"
import { Review } from "../../src/kilocode/review/review"
import { SessionID } from "../../src/session/schema"
import { SessionPrompt } from "../../src/session/prompt"
import { tmpdir } from "../fixture/fixture"
void Log.init({ print: false })
async function withInstance(fn: (dir: string) => Promise<void>) {
await using tmp = await tmpdir({ git: true })
await $`git branch main`.cwd(tmp.path).quiet().nothrow()
await Instance.provide({ directory: tmp.path, fn: () => fn(tmp.path) })
}
async function wait(sessionID: SessionID) {
for (const _ of Array.from({ length: 50 })) {
const list = await Question.list()
const question = list.find((item) => item.sessionID === sessionID)
if (question) return question
await Bun.sleep(10)
}
throw new Error("timed out waiting for question")
}
function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service>) {
return Effect.runPromise(fx.pipe(Effect.scoped, Effect.provide(SessionPrompt.defaultLayer)))
}
describe("local-review base branch", () => {
test("built-in local-review asks for a base branch before continuing", () =>
withInstance(async () => {
const sessionID = SessionID.make("ses_local_review_base")
const pending = run(
Effect.gen(function* () {
const prompt = yield* SessionPrompt.Service
return yield* prompt.command({
sessionID,
command: "local-review",
arguments: "",
})
}),
).catch((err) => err)
const question = await wait(sessionID)
expect(question.blocking).toBe(true)
expect(question.questions).toHaveLength(1)
expect(question.questions[0]?.header).toBe("Base branch")
expect(question.questions[0]?.custom).toBe(true)
expect(question.questions[0]?.options[0]?.label).toBe("main")
await Question.reject(question.id)
expect(await pending).toBeInstanceOf(Question.RejectedError)
expect(await Question.list()).toEqual([])
}))
test("base resolver returns a typed custom branch", () =>
withInstance(async () => {
const sessionID = SessionID.make("ses_local_review_custom")
const pending = ReviewBranch.resolve({ sessionID })
const question = await wait(sessionID)
await Question.reply({
requestID: question.id,
answers: [[" release/next "]],
})
await expect(pending).resolves.toBe("release/next")
expect(await Question.list()).toEqual([])
}))
test("branch prompt uses the provided base branch", () =>
withInstance(async (dir) => {
await $`git branch release`.cwd(dir).quiet()
await $`git checkout -b feature`.cwd(dir).quiet()
await Bun.write(path.join(dir, "feature.txt"), "feature\n")
await $`git add feature.txt`.cwd(dir).quiet()
await $`git commit -m "feature"`.cwd(dir).quiet()
const prompt = await Review.buildReviewPromptBranch("release")
expect(prompt).toContain("**branch diff**: `feature` -> `release`")
expect(prompt).toContain("These are the commits on `feature` since diverging from `release`:")
expect(prompt).toContain("`git diff release...feature`")
expect(prompt).toContain("`git log release..feature --oneline`")
}))
})