Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7ffdb86
feat: add one-click apply fixes suggestions to review mode
alex-alecu Feb 17, 2026
d86fa15
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 17, 2026
86769a4
fix: add kilocode_change marker to modified line in question-dock.tsx
alex-alecu Feb 17, 2026
ab34ac2
fix: replace non-existent 'architect' mode with 'orchestrator' in que…
alex-alecu Feb 17, 2026
2cebe2f
fix: replace polling loop with reactive effect and lifecycle cleanup …
alex-alecu Feb 17, 2026
6be0201
fix: remove stale NEEDS DISCUSSION reference from review prompt workf…
alex-alecu Feb 17, 2026
8f579ac
fix(app): wrap waitForIdle reactive effect in createRoot for proper d…
alex-alecu Feb 17, 2026
ab0381f
feat(app): add toast notifications for mode switching
alex-alecu Feb 17, 2026
f3ce168
fix: remove max option limit from review question tool instructions
alex-alecu Feb 17, 2026
b6d61a8
fix(app): pass description through mode action and delay toast
alex-alecu Feb 17, 2026
1e7111d
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 17, 2026
f0c1921
fix(app): fix disposal race condition in waitForIdle by using ref object
alex-alecu Feb 17, 2026
e0f5add
Merge branch 'feat/review-apply-fixes' of github.com:Kilo-Org/kilo in…
alex-alecu Feb 17, 2026
c85beb6
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 17, 2026
2ce796d
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 17, 2026
277be56
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 18, 2026
0baa8ea
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 18, 2026
1b1aa74
fix: Fix leak
alex-alecu Feb 18, 2026
c88fc52
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 18, 2026
36d6502
fix: Improve error handling
alex-alecu Feb 18, 2026
9d43ad4
fix: Update translations
alex-alecu Feb 18, 2026
40c412f
Merge branch 'dev' into feat/review-apply-fixes
alex-alecu Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions packages/app/src/components/question-dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type { QuestionAnswer, QuestionRequest } from "@kilocode/sdk/v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"

export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
// kilocode_change start - add onModeAction prop for mode-switching support
export const QuestionDock: Component<{
request: QuestionRequest
onModeAction?: (input: { mode: string; text: string; description?: string }) => void
}> = (props) => {
// kilocode_change end
const sdk = useSDK()
const language = useLanguage()

Expand Down Expand Up @@ -39,13 +44,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
}

const reply = (answers: QuestionAnswer[]) => {
if (store.sending) return
if (store.sending) return undefined

setStore("sending", true)
sdk.client.question
.reply({ requestID: props.request.id, answers })
.catch(fail)
.finally(() => setStore("sending", false))
return sdk.client.question.reply({ requestID: props.request.id, answers }).finally(() => setStore("sending", false))
}

const reject = () => {
Expand All @@ -59,10 +61,15 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
}

const submit = () => {
reply(questions().map((_, i) => store.answers[i] ?? []))
reply(questions().map((_, i) => store.answers[i] ?? []))?.catch(fail) // kilocode_change
}

const pick = (answer: string, custom: boolean = false) => {
// kilocode_change start - find option to check for mode
// Custom answers won't match a predefined option, so mode switching is intentionally skipped
const option = options().find((o) => o.label === answer)
// kilocode_change end

const answers = [...store.answers]
answers[store.tab] = [answer]
setStore("answers", answers)
Expand All @@ -74,7 +81,17 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
}

if (single()) {
reply([[answer]])
// kilocode_change start - trigger mode switch after question reply completes
const pending = reply([[answer]])
if (option?.mode && props.onModeAction) {
const action = props.onModeAction
const mode = option.mode
const description = option.description
pending?.then(() => action({ mode, text: answer, description }), fail).catch(fail)
} else {
pending?.catch(fail)
}
// kilocode_change end
return
}

Expand Down Expand Up @@ -194,6 +211,13 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
<Show when={opt.description}>
<span data-slot="option-description">{opt.description}</span>
</Show>
{/* kilocode_change start - show mode badge */}
<Show when={opt.mode}>
<span data-slot="option-mode" class="text-text-weakest text-11-regular">
→ {opt.mode}
</span>
</Show>
{/* kilocode_change end */}
<Show when={picked()}>
<Icon name="check-small" size="normal" />
</Show>
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,11 @@ export const dict = {

"session.context.addToContext": "Add {{selection}} to context",

"session.modeSwitch.switching": "Switching to {{mode}} mode…",
"session.modeSwitch.waiting": "Waiting for current task to complete",
"session.modeSwitch.notAvailable": "Agent not available",
"session.modeSwitch.fallback": '"{{requested}}" not found, using "{{actual}}"',

"session.new.worktree.main": "Main branch",
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
"session.new.worktree.create": "Create new worktree",
Expand Down
104 changes: 103 additions & 1 deletion packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, createRoot, on } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { Dynamic } from "solid-js/web"
Expand Down Expand Up @@ -39,6 +39,7 @@ import { usePermission } from "@/context/permission"
import { showToast } from "@opencode-ai/ui/toast"
import { SessionHeader, SessionContextTab, SortableTab, FileVisual, NewSessionView } from "@/components/session"
import { navMark, navParams } from "@/utils/perf"
import { Identifier } from "@/utils/id" // kilocode_change
import { same } from "@/utils/same"
import { createOpenReviewFile, focusTerminalById, getTabReorderIndex } from "@/pages/session/helpers"
import { createScrollSpy } from "@/pages/session/scroll-spy"
Expand Down Expand Up @@ -150,6 +151,106 @@ export default function Page() {
})
.finally(() => setUi("responding", false))
}

// kilocode_change start - handle mode switch from question options
let modeActionAbort: AbortController | undefined

const waitForIdle = (sessionID: string, signal: AbortSignal) =>
new Promise<void>((resolve, reject) => {
let settled = false
const ref: { dispose?: () => void } = {}
const settle = (fn: () => void) => {
if (settled) return
settled = true
clearTimeout(timeout)
ref.dispose?.()
fn()
}
const timeout = setTimeout(() => {
settle(() => reject(new Error("Timed out waiting for session idle")))
}, 30_000)

createRoot((dispose) => {
ref.dispose = dispose
signal.addEventListener("abort", () => settle(() => reject(new Error("Cancelled"))), { once: true })
createEffect(() => {
const status = sync.data.session_status[sessionID]
if (!status || status.type !== "idle") return
settle(() => resolve())
})
})
})

onCleanup(() => modeActionAbort?.abort())

const handleModeAction = async (input: { mode: string; text: string; description?: string }) => {
const sessionID = params.id
if (!sessionID) return

modeActionAbort?.abort()
const controller = new AbortController()
modeActionAbort = controller

const toastTimer = setTimeout(() => {
showToast({
title: language.t("session.modeSwitch.switching", { mode: input.mode }),
description: language.t("session.modeSwitch.waiting"),
})
}, 500)

try {
// Allow one microtask for session status to reflect the reply before checking idle
await new Promise((r) => setTimeout(r, 0))
await waitForIdle(sessionID, controller.signal)
} catch (err: unknown) {
clearTimeout(toastTimer)
if (controller.signal.aborted) return
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
return
}

clearTimeout(toastTimer)

if (controller.signal.aborted) return

local.agent.set(input.mode)

const agent = local.agent.current()
if (!agent) return

if (agent.name !== input.mode) {
showToast({
title: language.t("session.modeSwitch.notAvailable"),
description: language.t("session.modeSwitch.fallback", { requested: input.mode, actual: agent.name }),
})
}

const model = local.model.current()
if (!model) return

const variant = local.model.variant.current()
const messageID = Identifier.ascending("message")

sdk.client.session
.prompt({
sessionID,
agent: agent.name,
model: {
modelID: model.id,
providerID: model.provider.id,
},
messageID,
parts: [{ type: "text", text: input.description ?? input.text }],
variant,
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
// kilocode_change end

const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const workspaceKey = createMemo(() => params.dir ?? "")
const workspaceTabs = createMemo(() => layout.tabs(workspaceKey))
Expand Down Expand Up @@ -1697,6 +1798,7 @@ export default function Page() {
resumeScroll()
}}
setPromptDockRef={(el) => (promptDock = el)}
onModeAction={handleModeAction}
/>

<Show when={desktopReviewOpen()}>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/pages/session/session-prompt-dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function SessionPromptDock(props: {
onNewSessionWorktreeReset: () => void
onSubmit: () => void
setPromptDockRef: (el: HTMLDivElement) => void
onModeAction?: (input: { mode: string; text: string; description?: string }) => void // kilocode_change
}) {
return (
<div
Expand All @@ -48,7 +49,7 @@ export function SessionPromptDock(props: {
subtitle,
}}
/>
<QuestionDock request={questionDockRequest(req)} />
<QuestionDock request={questionDockRequest(req)} onModeAction={props.onModeAction} />
</div>
)
}}
Expand Down
33 changes: 32 additions & 1 deletion packages/opencode/src/kilocode/review/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,38 @@ One of:
- **APPROVE** — Code is ready to merge/commit
- **APPROVE WITH SUGGESTIONS** — Minor improvements suggested but not blocking
- **NEEDS CHANGES** — Issues must be addressed before merging
- **NEEDS DISCUSSION** — Architectural or design concerns need team input

## IMPORTANT: Post-Review Workflow

You MUST first write the COMPLETE review above (Summary, Issues Found, Detailed Findings, Recommendation) as regular text output. Do NOT use the question tool until the entire review text has been written.

ONLY AFTER the full review is written:

- If your recommendation is **APPROVE** with no issues found, you are done. Do NOT call the question tool.
- If your recommendation is **APPROVE WITH SUGGESTIONS** or **NEEDS CHANGES**, THEN call the question tool to offer fix suggestions with mode switching.

When calling the question tool, provide at least one option. Choose the appropriate mode for each option:
- mode "code" for direct code fixes (bugs, missing error handling, clear improvements)
- mode "debug" for issues needing investigation before fixing (race conditions, unclear root causes, intermittent failures)
- mode "orchestrator" when there are many issues (5+) spanning different categories that need coordinated, planned fixes

Option patterns based on review findings:
- **Few clear fixes (1-4 issues, same category):** offer mode "code" fixes
- **Many issues across categories (5+, mixed security/performance/quality):** offer mode "orchestrator" to plan fixes and mode "code" for quick wins
- **Issues needing investigation:** include a mode "debug" option to investigate root causes
- **Suggestions only:** offer mode "code" to apply improvements

Example question tool call (ONLY after full review is written):
{
"questions": [{
"question": "What would you like to do?",
"header": "Next steps",
"options": [
{ "label": "Fix all issues", "description": "Fix all issues found in this review", "mode": "code" },
{ "label": "Fix critical only", "description": "Fix critical issues only", "mode": "code" }
]
}]
}
`

const EMPTY_DIFF_PROMPT = `You are Kilo Code, an expert code reviewer with deep expertise in software engineering best practices, security vulnerabilities, performance optimization, and code quality. Your role is advisory — provide clear, actionable feedback but DO NOT modify any files. Do not use any file editing tools.
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/question/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export namespace Question {
.object({
label: z.string().describe("Display text (1-5 words, concise)"),
description: z.string().describe("Explanation of choice"),
mode: z
.string()
.optional()
.describe("Optional agent/mode to switch to when selected (e.g. code, debug, orchestrator)"), // kilocode_change
})
.meta({
ref: "QuestionOption",
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,10 @@ export type QuestionOption = {
* Explanation of choice
*/
description: string
/**
* Optional agent/mode to switch to when selected (e.g. code, debug, orchestrator)
*/
mode?: string
}

export type QuestionInfo = {
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7786,6 +7786,10 @@
"description": {
"description": "Explanation of choice",
"type": "string"
},
"mode": {
"description": "Optional agent/mode to switch to when selected (e.g. code, debug, orchestrator)",
"type": "string"
}
},
"required": ["label", "description"]
Expand Down
Loading