From 0e1f11bed6b243f5f9379ecf05f68577e666e87a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 12:48:24 +0200 Subject: [PATCH 1/2] feat(review): add nested suggestions and subcommands for review --- .changeset/optimize-review-slash-commands.md | 6 +++ .../tests/unit/use-slash-command.test.ts | 45 +++++++++++++++++ .../webview-ui/src/hooks/useSlashCommand.ts | 48 ++++++++++++++++--- packages/opencode/src/command/index.ts | 2 - .../opencode/src/kilocode/review/command.ts | 2 +- .../opencode/src/kilocode/review/review.txt | 30 ++++++++---- .../opencode/src/kilocode/suggestion/tool.txt | 3 +- .../test/kilocode/review-command.test.ts | 19 ++++++-- 8 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 .changeset/optimize-review-slash-commands.md diff --git a/.changeset/optimize-review-slash-commands.md b/.changeset/optimize-review-slash-commands.md new file mode 100644 index 00000000000..8e104765c23 --- /dev/null +++ b/.changeset/optimize-review-slash-commands.md @@ -0,0 +1,6 @@ +--- +"kilo-code": minor +"@kilocode/cli": minor +--- + +Add nested slash command suggestions for `/review` in VS Code and support `staged`, `unpushed`, and `quick` review modes. diff --git a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts index 710ca744bef..bc4c1aa6f11 100644 --- a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts +++ b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts @@ -188,4 +188,49 @@ describe("useSlashCommand sandbox action", () => { expect(ctx.slash.results()[0]?.description).toBe("Toggle sandbox") ctx.dispose() }) + + it("opens review options from the top-level command", () => { + const ctx = setup(() => {}) + const state = { text: "/review" } + const textarea = { + value: state.text, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + + ctx.slash.onInput("/rev", 4) + + expect(ctx.slash.results()).toContainEqual( + expect.objectContaining({ name: "review", description: expect.stringContaining("Review code changes") }), + ) + ctx.slash.select(ctx.slash.results().find((c) => c.name === "review")!, textarea, (text) => (state.text = text)) + expect(state.text).toBe("/review ") + expect(ctx.slash.results().map((command) => command.name)).toEqual([ + "review uncommitted", + "review staged", + "review unpushed", + "review branch", + "review quick", + ]) + ctx.dispose() + }) + + it("completes nested review actions and closes for free text", () => { + const ctx = setup(() => {}) + const state = { text: "/review unp" } + const textarea = { + value: state.text, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + + ctx.slash.onInput(state.text, state.text.length) + expect(ctx.slash.results().map((command) => command.name)).toEqual(["review unpushed"]) + ctx.slash.select(ctx.slash.results()[0]!, textarea, (text) => (state.text = text)) + expect(state.text).toBe("/review unpushed ") + + ctx.slash.onInput("/review focus on auth", 20) + expect(ctx.slash.show()).toBe(false) + ctx.dispose() + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index 6b99617a3b1..b59b48a58af 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -144,6 +144,21 @@ export function useSlashCommand( { name: "memory auto on", description: "Enable automatic memory saves", hints: [] }, { name: "memory auto off", description: "Disable automatic memory saves", hints: [] }, { name: "memory purge confirm", description: "Delete all project memory files", hints: [] }, + { + name: "review", + description: "Review code changes [uncommitted, staged, unpushed, branch, pr]", + hints: ["code-review", "diff"], + nested: true, + }, + { name: "review uncommitted", description: "Review uncommitted changes (staged, unstaged, untracked)", hints: [] }, + { name: "review staged", description: "Review staged changes only", hints: [] }, + { name: "review unpushed", description: "Review local commits ahead of upstream", hints: [] }, + { name: "review branch", description: "Review current branch against base branch", hints: [] }, + { + name: "review quick", + description: "Fast single-pass review with minimal token usage", + hints: ["--quick", "fast"], + }, { name: "export", description: "Export the current session transcript as Markdown", @@ -240,6 +255,15 @@ export function useSlashCommand( lower, ) } + if (q.startsWith("review ")) { + const matches = list.filter((cmd) => cmd.name.startsWith("review ")) + if (q === "review ") return matches + const lower = q.toLowerCase() + return sortByScore( + matches.filter((cmd) => cmd.name.toLowerCase().startsWith(lower)), + lower, + ) + } const root = list.filter((cmd) => !cmd.name.includes(" ")) if (!q) return root const lower = q.toLowerCase() @@ -275,12 +299,24 @@ export function useSlashCommand( return } const memory = before.match(/^\/(?:memory|mem)\s+([^\n]*)$/i) - if (!memory) return close() - const value = `memory ${memory[1]}`.toLowerCase() - if (!commands().some((cmd) => cmd.name.toLowerCase().startsWith(value))) return close() - request() - setQuery(value) - setIndex(0) + if (memory) { + const value = `memory ${memory[1]}`.toLowerCase() + if (!commands().some((cmd) => cmd.name.toLowerCase().startsWith(value))) return close() + request() + setQuery(value) + setIndex(0) + return + } + const review = before.match(/^\/review\s+([^\n]*)$/i) + if (review) { + const value = `review ${review[1]}`.toLowerCase() + if (!commands().some((cmd) => cmd.name.toLowerCase().startsWith(value))) return close() + request() + setQuery(value) + setIndex(0) + return + } + return close() } const select = ( diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index a93495c4174..3cfd252ee71 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -115,8 +115,6 @@ const layer = Layer.effect( } // kilocode_change start commands[Default.REVIEW] = reviewCommand() - commands["local-review"] = legacyReviewCommand("local-review")! - commands["local-review-uncommitted"] = legacyReviewCommand("local-review-uncommitted")! commands["resume-claude"] = SessionResume.resumeClaude commands["resume-codex"] = SessionResume.resumeCodex // kilocode_change end diff --git a/packages/opencode/src/kilocode/review/command.ts b/packages/opencode/src/kilocode/review/command.ts index 646ccd1d1d4..bfb50b736f4 100644 --- a/packages/opencode/src/kilocode/review/command.ts +++ b/packages/opencode/src/kilocode/review/command.ts @@ -30,7 +30,7 @@ export function parseReviewCommand(prompt: string | undefined): ReviewCommand | export function reviewCommand(): Command.Info { return { name: "review", - description: "review changes [uncommitted|commit|branch|pr]", + description: "review changes [uncommitted|staged|unpushed|branch|pr]", template: REVIEW, hints: ["$ARGUMENTS"], } diff --git a/packages/opencode/src/kilocode/review/review.txt b/packages/opencode/src/kilocode/review/review.txt index d9ce74ce371..afdca81d3cc 100644 --- a/packages/opencode/src/kilocode/review/review.txt +++ b/packages/opencode/src/kilocode/review/review.txt @@ -1,6 +1,6 @@ You are Kilo Code, an expert code reviewer focused on high-confidence security, performance, business logic, deploy safety, duplication, and dead-code findings. During the initial review phase, your role is advisory: provide clear, actionable feedback but DO NOT modify any files. Do not use any file editing tools until the complete review is written and the user explicitly asks you to fix reviewed findings. -You are performing a code review with `/review`. It supports uncommitted working-tree changes, a specific commit, the current branch against a base ref, or a GitHub pull request. +You are performing a code review with `/review`. It supports uncommitted working-tree changes, staged changes, unpushed commits, a specific commit, the current branch against a base ref, or a GitHub pull request. --- @@ -12,18 +12,20 @@ $ARGUMENTS ## Interpreting User Input -Treat the user input above as the literal free-form text the user typed after `/review`. It can be empty, review guidance, an explicit local scope, a commit hash, a branch or base ref, or a pull request URL or number. +Treat the user input above as the literal free-form text the user typed after `/review`. It can be empty, review guidance, an explicit local scope (`uncommitted`, `staged`, `unpushed`, `branch`), effort flags (`quick`, `--quick`, `-q`, `deep`, `--deep`, `-d`, `--effort <1-10>`), a commit hash, a branch or base ref, or a pull request URL or number. Choose exactly one review scope in this order: -1. **Explicit uncommitted scope** - `/review uncommitted [guidance]` reviews staged, unstaged, and untracked changes. Phrases that clearly request working-tree, staged, unstaged, uncommitted, or untracked changes select the same scope. -2. **Explicit branch scope** - `/review branch [base] [guidance]` reviews the current branch against the provided base, or against the default base when none is provided. After `branch`, treat a token as the base only when it resolves as a git ref or is identified with syntax such as `base=`, `base `, `against `, `compare to `, or `vs `; otherwise treat it as guidance. Phrases that clearly request branch, committed, or PR-ready changes select branch scope. -3. **Commit** - a 7-40 character hexadecimal token that resolves as a commit selects commit review. Treat remaining text as guidance. -4. **Pull request** - input that starts with a GitHub pull request URL or a positive PR number selects pull request review. Treat remaining text as guidance. -5. **Branch or base ref** - a token that resolves as a local or remote git ref, or a clearly named base such as `base main`, `against origin/dev`, `compare to develop`, or `vs release/next`, selects branch review. Treat remaining text as guidance. -6. **Empty or guidance-only input** - choose uncommitted review. Bare `/review` always defaults to uncommitted changes, even when the working tree is clean. Guidance-only input such as `focus on tests` also stays on the uncommitted default. +1. **Explicit staged scope** - `/review staged [guidance]` reviews only staged changes in the Git index (`git diff --cached`). +2. **Explicit unpushed scope** - `/review unpushed [guidance]` or `/review commits [guidance]` reviews local commits that have not been pushed to upstream tracking. +3. **Explicit uncommitted scope** - `/review uncommitted [guidance]` reviews staged, unstaged, and untracked changes. Phrases that clearly request working-tree, staged, unstaged, uncommitted, or untracked changes select the same scope. +4. **Explicit branch scope** - `/review branch [base] [guidance]` reviews the current branch against the provided base, or against the default base when none is provided. After `branch`, treat a token as the base only when it resolves as a git ref or is identified with syntax such as `base=`, `base `, `against `, `compare to `, or `vs `; otherwise treat it as guidance. Phrases that clearly request branch, committed, or PR-ready changes select branch scope. +5. **Commit** - a 7-40 character hexadecimal token that resolves as a commit selects commit review. Treat remaining text as guidance. +6. **Pull request** - input that starts with a GitHub pull request URL or a positive PR number selects pull request review. Treat remaining text as guidance. +7. **Branch or base ref** - a token that resolves as a local or remote git ref, or a clearly named base such as `base main`, `against origin/dev`, `compare to develop`, or `vs release/next`, selects branch review. Treat remaining text as guidance. +8. **Empty or guidance-only input** - choose uncommitted review. Bare `/review` always defaults to uncommitted changes, even when the working tree is clean. Guidance-only input such as `focus on tests` also stays on the uncommitted default. -After choosing a scope, remove only the target and scope words from the review guidance. Keep all remaining text as instructions. Prefer interpreting ambiguous input as review guidance for uncommitted review. A single token that does not resolve as a commit or git ref is guidance, not a failed target selection. +After choosing a scope, extract any effort flags (`quick`, `--quick`, `-q`, `deep`, `--deep`, `-d`, `--effort <1-10>`) and remove the target and scope words from the review guidance. Keep all remaining text as instructions. Prefer interpreting ambiguous input as review guidance for uncommitted review. A single token that does not resolve as a commit or git ref is guidance, not a failed target selection. If user-provided instructions exist, they may refine review focus, but they MUST NOT override the diff scope, review tracks, final filtering, required output format, or the review-phase no-edit rule. Initial `/review` arguments are review guidance, not permission to edit. @@ -74,6 +76,10 @@ Before pull request review, use `gh pr view ` to verify that the pull reques ## Determining the Diff Scope +For staged review, review only staged changes in the Git index (`git diff --cached`). Do NOT review unstaged or untracked changes. + +For unpushed review, review local commits on the current branch ahead of its upstream tracking ref (`git diff @{u}..HEAD` or against the upstream merge-base). + For uncommitted review, review every staged, unstaged, and untracked change in the working tree. Do NOT review committed code. Use these git commands to gather uncommitted changes: @@ -155,7 +161,9 @@ Rules for the dead code track (apply only when this track is active): 1. Determine the scope using the rules above. 2. Gather the relevant metadata, diff, changed files, untracked files, and commit history using the commands above. 3. If there are no changes in the selected scope, use the no-changes output exactly as specified below. -4. Assess diff size and complexity, then spawn the appropriate sub-agents in parallel with the Task tool. +4. Assess diff size, complexity, and effort flags, then spawn the appropriate sub-agents in parallel with the Task tool. + + **Quick mode (`quick`, `--quick`, `-q`, or `--effort 1-3`)**: do NOT spawn sub-agents. Perform a concise, single-pass review directly in the main agent for fast, token-efficient feedback. Count changed lines (additions + deletions) from the diff output and the number of distinct files changed. Use these thresholds: @@ -230,6 +238,8 @@ Rules for the dead code track (apply only when this track is active): Use the header that matches the selected scope: +- Staged: `## Local Review for **staged changes**` +- Unpushed: `## Local Review for **unpushed commits**` - Uncommitted: `## Local Review for **uncommitted changes**` - Branch: `## Local Review for **branch diff**: \`\` -> \`\`` - Commit: `## Code Review for **commit**: \`\`` diff --git a/packages/opencode/src/kilocode/suggestion/tool.txt b/packages/opencode/src/kilocode/suggestion/tool.txt index 0e00267446f..5abdb200ed0 100644 --- a/packages/opencode/src/kilocode/suggestion/tool.txt +++ b/packages/opencode/src/kilocode/suggestion/tool.txt @@ -26,5 +26,6 @@ Do NOT suggest a review when: Choosing the right review prompt for the action prompt: - Use `/review uncommitted` as the action prompt for uncommitted working-tree changes (staged, unstaged, and untracked files) -- Use `/review branch` as the action prompt for committed branch-level changes +- Use `/review unpushed` as the action prompt for committed changes ahead of upstream +- Use `/review branch` as the action prompt for branch-level changes against base - Prefer `/review uncommitted` when the work you just did has not been committed yet diff --git a/packages/opencode/test/kilocode/review-command.test.ts b/packages/opencode/test/kilocode/review-command.test.ts index ec000bdc8c0..c9a9804bfeb 100644 --- a/packages/opencode/test/kilocode/review-command.test.ts +++ b/packages/opencode/test/kilocode/review-command.test.ts @@ -22,6 +22,10 @@ describe("review command parsing", () => { expect(parseReviewCommand("/review")).toBe("review") expect(parseReviewCommand("/review focus on tests")).toBe("review") expect(parseReviewCommand("/review uncommitted focus on tests")).toBe("review") + expect(parseReviewCommand("/review staged")).toBe("review") + expect(parseReviewCommand("/review unpushed")).toBe("review") + expect(parseReviewCommand("/review quick")).toBe("review") + expect(parseReviewCommand("/review --quick")).toBe("review") expect(parseReviewCommand("/review branch origin/main focus on auth")).toBe("review") expect(parseReviewCommand("/review a1b2c3d")).toBe("review") expect(parseReviewCommand("/review https://github.com/Kilo-Org/kilocode/pull/11084")).toBe("review") @@ -60,6 +64,14 @@ describe("review command", () => { expect(text).toContain("git ls-files --others --exclude-standard") }) + test("documents explicit staged and unpushed review", () => { + const text = cmd.template as string + expect(text).toContain("`/review staged [guidance]`") + expect(text).toContain("`/review unpushed [guidance]`") + expect(text).toContain("For staged review") + expect(text).toContain("For unpushed review") + }) + test("documents explicit and ref-based branch review", () => { const text = cmd.template as string expect(text).toContain("`/review branch [base] [guidance]`") @@ -133,6 +145,7 @@ describe("review command", () => { test("applies adaptive parallel review tracks", () => { const text = cmd.template as string + expect(text).toContain("Quick mode (`quick`, `--quick`, `-q`, or `--effort 1-3`)") expect(text).toContain("spawn the appropriate sub-agents in parallel") expect(text).toContain("do NOT spawn sub-agents") expect(text).toContain("spawn a single security sub-agent") @@ -144,7 +157,7 @@ describe("review command", () => { expect(text).toContain("NO_FINDINGS") }) - it.live("lists review and deprecated review aliases", () => + it.live("resolves review and deprecated review aliases", () => provideTmpdirInstance( () => Effect.gen(function* () { @@ -156,8 +169,8 @@ describe("review command", () => { const uncommitted = yield* command.get("local-review-uncommitted") expect(names).toContain("review") - expect(names).toContain("local-review") - expect(names).toContain("local-review-uncommitted") + expect(names).not.toContain("local-review") + expect(names).not.toContain("local-review-uncommitted") expect(review?.name).toBe("review") expect(branch?.description).toBe("deprecated; use /review branch") expect(branch?.template).toBe(legacyReviewMessage("local-review")) From 2b92036fca610f00b554c99f5c6c7c2e320f9a0a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 7 Aug 2026 12:59:05 +0200 Subject: [PATCH 2/2] fix(review): address review feedback for scopes, fallback, and deep mode --- packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts | 2 +- packages/opencode/src/kilocode/review/command.ts | 2 +- packages/opencode/src/kilocode/review/review.txt | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index b59b48a58af..d5192ad2a33 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -146,7 +146,7 @@ export function useSlashCommand( { name: "memory purge confirm", description: "Delete all project memory files", hints: [] }, { name: "review", - description: "Review code changes [uncommitted, staged, unpushed, branch, pr]", + description: "Review code changes [uncommitted, staged, unpushed, branch, commit, pr]", hints: ["code-review", "diff"], nested: true, }, diff --git a/packages/opencode/src/kilocode/review/command.ts b/packages/opencode/src/kilocode/review/command.ts index bfb50b736f4..959404fea39 100644 --- a/packages/opencode/src/kilocode/review/command.ts +++ b/packages/opencode/src/kilocode/review/command.ts @@ -30,7 +30,7 @@ export function parseReviewCommand(prompt: string | undefined): ReviewCommand | export function reviewCommand(): Command.Info { return { name: "review", - description: "review changes [uncommitted|staged|unpushed|branch|pr]", + description: "review changes [uncommitted|staged|unpushed|branch|commit|pr]", template: REVIEW, hints: ["$ARGUMENTS"], } diff --git a/packages/opencode/src/kilocode/review/review.txt b/packages/opencode/src/kilocode/review/review.txt index afdca81d3cc..e65ae6f1a73 100644 --- a/packages/opencode/src/kilocode/review/review.txt +++ b/packages/opencode/src/kilocode/review/review.txt @@ -78,7 +78,7 @@ Before pull request review, use `gh pr view ` to verify that the pull reques For staged review, review only staged changes in the Git index (`git diff --cached`). Do NOT review unstaged or untracked changes. -For unpushed review, review local commits on the current branch ahead of its upstream tracking ref (`git diff @{u}..HEAD` or against the upstream merge-base). +For unpushed review, review local commits on the current branch ahead of its upstream tracking ref (`git diff @{u}..HEAD`). If no upstream tracking ref is configured (e.g. on a fresh branch), fall back to comparing against the default base branch (`git diff $(git merge-base HEAD )..HEAD`). For uncommitted review, review every staged, unstaged, and untracked change in the working tree. Do NOT review committed code. @@ -165,6 +165,8 @@ Rules for the dead code track (apply only when this track is active): **Quick mode (`quick`, `--quick`, `-q`, or `--effort 1-3`)**: do NOT spawn sub-agents. Perform a concise, single-pass review directly in the main agent for fast, token-efficient feedback. + **Deep mode (`deep`, `--deep`, `-d`, or `--effort 8-10`)**: spawn specialized sub-agents across all six tracks (security, performance, business logic, deploy safety, duplication, dead code) for an in-depth audit regardless of diff size. + Count changed lines (additions + deletions) from the diff output and the number of distinct files changed. Use these thresholds: **Small** (< 100 changed lines AND ≤ 3 files): do NOT spawn sub-agents. Review the relevant tracks yourself in the main agent. The only exception is security-sensitive changes (such as auth, secrets, crypto, input validation, or access control): spawn a single security sub-agent and review the other relevant tracks yourself.