diff --git a/.changeset/fix-question-multiselect-space-toggle.md b/.changeset/fix-question-multiselect-space-toggle.md
new file mode 100644
index 00000000000..4720dc5ade7
--- /dev/null
+++ b/.changeset/fix-question-multiselect-space-toggle.md
@@ -0,0 +1,5 @@
+---
+"@kilocode/cli": patch
+---
+
+Bind `Space` to toggle and `Enter` to continue in multi-select questions from the `kilo run` footer and the interactive TUI, matching the convention used by other CLI tools (`fzf`, `lazygit`, `btop`, ...).
\ No newline at end of file
diff --git a/packages/opencode/src/cli/cmd/run/footer.question.tsx b/packages/opencode/src/cli/cmd/run/footer.question.tsx
index bafb2c4676b..9177e9e8ab7 100644
--- a/packages/opencode/src/cli/cmd/run/footer.question.tsx
+++ b/packages/opencode/src/cli/cmd/run/footer.question.tsx
@@ -38,6 +38,7 @@ import {
questionSubmit,
questionSync,
questionTabs,
+ questionToggleAction, // kilocode_change
questionTotal,
} from "./question.shared"
import type { RunFooterTheme } from "./theme"
@@ -65,7 +66,7 @@ export function RunQuestionBody(props: {
}
if (info()?.multiple) {
- return "toggle"
+ return "continue" // kilocode_change
}
if (single()) {
@@ -125,7 +126,10 @@ export function RunQuestionBody(props: {
const choose = (selected: number) => {
const base = state()
const cur = questionSetSelected(base, selected)
- const next = questionSelect(cur, props.request)
+ // kilocode_change start - digit shortcuts toggle in multi-select
+ const info = questionInfo(props.request, cur)
+ const next = info?.multiple ? questionToggleAction(cur, props.request) : questionSelect(cur, props.request)
+ // kilocode_change end
if (next.state !== base) {
setState(next.state)
}
@@ -217,6 +221,17 @@ export function RunQuestionBody(props: {
return
}
+ // kilocode_change start - space toggles in multi-select
+ if (event.name === "space") {
+ const next = questionToggleAction(cur, props.request)
+ if (next.state !== cur) {
+ setState(next.state)
+ }
+ event.preventDefault()
+ return
+ }
+ // kilocode_change end
+
const total = questionTotal(props.request, cur)
const max = Math.min(total, 9)
const digit = Number(event.name)
@@ -562,6 +577,13 @@ export function RunQuestionBody(props: {
{"⇆"} tab
+ {/* kilocode_change start - multi-select space toggle hint */}
+
+
+ space toggle
+
+
+ {/* kilocode_change end */}
{"↑↓"} select
diff --git a/packages/opencode/src/cli/cmd/run/question.shared.ts b/packages/opencode/src/cli/cmd/run/question.shared.ts
index 575c24f9662..5519f7be0b8 100644
--- a/packages/opencode/src/cli/cmd/run/question.shared.ts
+++ b/packages/opencode/src/cli/cmd/run/question.shared.ts
@@ -216,6 +216,7 @@ export function questionMove(state: QuestionBodyState, request: QuestionRequest,
}
}
+// kilocode_change start - space toggles, enter advances in multi-select
export function questionSelect(state: QuestionBodyState, request: QuestionRequest): QuestionStep {
const info = questionInfo(request, state)
if (!info) {
@@ -229,8 +230,38 @@ export function questionSelect(state: QuestionBodyState, request: QuestionReques
}
}
+ const last = request.questions.length - 1
+ const next = state.tab >= last ? last + 1 : state.tab + 1
+ return {
+ state: questionSetTab(state, next),
+ }
+ }
+
+ const option = info.options[state.selected]
+ if (!option) {
+ return { state }
+ }
+
+ if (info.multiple) {
+ const last = request.questions.length - 1
+ const next = state.tab >= last ? last + 1 : state.tab + 1
+ return {
+ state: questionSetTab(state, next),
+ }
+ }
+
+ return questionPick(state, request, option.label)
+}
+
+export function questionToggleAction(state: QuestionBodyState, request: QuestionRequest): QuestionStep {
+ const info = questionInfo(request, state)
+ if (!info || !info.multiple) {
+ return { state }
+ }
+
+ if (questionOther(request, state)) {
const value = questionInput(state)
- if (value && questionPicked(state)) {
+ if (value) {
return {
state: questionToggle(state, value),
}
@@ -246,14 +277,11 @@ export function questionSelect(state: QuestionBodyState, request: QuestionReques
return { state }
}
- if (info.multiple) {
- return {
- state: questionToggle(state, option.label),
- }
+ return {
+ state: questionToggle(state, option.label),
}
-
- return questionPick(state, request, option.label)
}
+// kilocode_change end
export function questionSave(state: QuestionBodyState, request: QuestionRequest): QuestionStep {
const info = questionInfo(request, state)
@@ -332,9 +360,15 @@ export function questionHint(request: QuestionRequest, state: QuestionBodyState)
}
const info = questionInfo(request, state)
+ // kilocode_change start - multi-select hint uses space/continue
if (questionSingle(request)) {
- return `↑↓ select enter ${info?.multiple ? "toggle" : "submit"} esc dismiss`
+ return `↑↓ select enter submit esc dismiss`
+ }
+
+ if (info?.multiple) {
+ return `⇆ tab ↑↓ select space toggle enter continue esc dismiss`
}
- return `⇆ tab ↑↓ select enter ${info?.multiple ? "toggle" : "confirm"} esc dismiss`
+ return `⇆ tab ↑↓ select enter confirm esc dismiss`
+ // kilocode_change end
}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
index 1cba4fc92b9..979dec906b2 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
@@ -128,6 +128,26 @@ export function QuestionPrompt(props: {
pick(opt.label)
}
+ // kilocode_change start - space toggles, enter advances in multi-select
+ function continueOption() {
+ if (other()) {
+ if (multi()) {
+ selectTab(Math.min(store.tab + 1, tabs() - 1))
+ return
+ }
+ setStore("editing", true)
+ return
+ }
+ if (multi()) {
+ selectTab(Math.min(store.tab + 1, tabs() - 1))
+ return
+ }
+ const opt = options()[store.selected]
+ if (!opt) return
+ pick(opt.label)
+ }
+ // kilocode_change end
+
onMount(() => {
const popMode = modeStack.push(QUESTION_MODE)
onCleanup(popMode)
@@ -259,15 +279,36 @@ export function QuestionPrompt(props: {
...tuiConfig.keybinds.get("app.exit"),
]
: [
+ // kilocode_change start - space toggles, enter advances in multi-select
...Array.from({ length: max }, (_, index) => ({
key: String(index + 1),
- desc: `Select answer ${index + 1}`,
+ desc: question()?.multiple ? `Toggle answer ${index + 1}` : `Select answer ${index + 1}`,
group: "Question",
cmd: () => {
moveTo(index)
- selectOption()
+ if (index === options().length) {
+ if (question()?.multiple) {
+ const value = input()
+ if (value) {
+ toggle(value)
+ } else {
+ setStore("editing", true)
+ }
+ } else {
+ setStore("editing", true)
+ }
+ return
+ }
+ const opt = options()[index]
+ if (!opt) return
+ if (question()?.multiple) {
+ toggle(opt.label)
+ } else {
+ pick(opt.label)
+ }
},
})),
+ // kilocode_change end
{
key: "up",
desc: "Previous answer",
@@ -282,7 +323,39 @@ export function QuestionPrompt(props: {
},
{ key: "down", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) },
{ key: "j", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) },
- { key: "return", desc: "Select answer", group: "Question", cmd: () => selectOption() },
+ // kilocode_change start - multi-select space toggle binding
+ ...(question()?.multiple
+ ? [
+ {
+ key: "space",
+ desc: "Toggle answer",
+ group: "Question",
+ cmd: () => {
+ if (other()) {
+ const value = input()
+ if (value) {
+ toggle(value)
+ } else {
+ setStore("editing", true)
+ }
+ return
+ }
+ const opt = options()[store.selected]
+ if (!opt) return
+ toggle(opt.label)
+ },
+ },
+ ]
+ : []),
+ // kilocode_change end
+ // kilocode_change start - enter advances in multi-select
+ {
+ key: "return",
+ desc: question()?.multiple ? "Continue" : "Select answer",
+ group: "Question",
+ cmd: () => continueOption(),
+ },
+ // kilocode_change end
{ key: "escape", desc: "Reject question", group: "Question", cmd: () => reject() },
...tuiConfig.keybinds.get("app.exit"),
]),
@@ -497,16 +570,24 @@ export function QuestionPrompt(props: {
{"⇆"} tab
+ {/* kilocode_change start - multi-select space toggle hint */}
+
+
+ space toggle
+
+
+ {/* kilocode_change end */}
{"↑↓"} select
- enter{" "}
+ enter {/* kilocode_change start - multi-select uses continue verb */}
- {confirm() ? "submit" : multi() ? "toggle" : single() ? "submit" : "confirm"}
+ {confirm() ? "submit" : multi() ? "continue" : single() ? "submit" : "confirm"}
+ {/* kilocode_change end */}
diff --git a/packages/opencode/test/cli/run/question.shared.test.ts b/packages/opencode/test/cli/run/question.shared.test.ts
index 39247e2d329..f0b1d6138ac 100644
--- a/packages/opencode/test/cli/run/question.shared.test.ts
+++ b/packages/opencode/test/cli/run/question.shared.test.ts
@@ -10,6 +10,7 @@ import {
questionStoreCustom,
questionSubmit,
questionSync,
+ questionToggleAction, // kilocode_change
} from "@/cli/cmd/run/question.shared"
function req(input: Partial = {}): QuestionRequest {
@@ -71,6 +72,7 @@ describe("run question shared", () => {
})
})
+ // kilocode_change start - space toggles, enter advances in multi-select
test("toggles answers for multiple-choice questions", () => {
const ask = req({
questions: [
@@ -83,13 +85,156 @@ describe("run question shared", () => {
],
})
- let state = questionSelect(createQuestionBodyState("question-1"), ask).state
+ let state = questionToggleAction(createQuestionBodyState("question-1"), ask).state
expect(state.answers).toEqual([["bug"]])
- state = questionSelect(state, ask).state
+ state = questionToggleAction(state, ask).state
expect(state.answers).toEqual([[]])
})
+ test("enter advances multi-select tab without toggling", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Tags?",
+ header: "Tags",
+ options: [{ label: "bug", description: "Bug fix" }],
+ multiple: true,
+ },
+ ],
+ })
+
+ const state = questionSelect(createQuestionBodyState("question-1"), ask).state
+ expect(state.answers).toEqual([])
+ expect(state.tab).toBe(1)
+ expect(questionConfirm(ask, state)).toBe(true)
+ })
+
+ test("enter advances multi-question single-select tab on the last question to confirm", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "First?",
+ header: "First",
+ options: [{ label: "a", description: "" }],
+ multiple: false,
+ },
+ {
+ question: "Second?",
+ header: "Second",
+ options: [{ label: "b", description: "" }],
+ multiple: false,
+ },
+ ],
+ })
+
+ let state = createQuestionBodyState("question-1")
+ state = questionSelect(state, ask).state
+ expect(state.tab).toBe(1)
+
+ state = questionSelect(state, ask).state
+ expect(questionConfirm(ask, state)).toBe(true)
+ })
+
+ test("toggle is a no-op in single-select", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Mode?",
+ header: "Mode",
+ options: [{ label: "chunked", description: "" }],
+ multiple: false,
+ },
+ ],
+ })
+
+ const state = questionToggleAction(createQuestionBodyState("question-1"), ask).state
+ expect(state.answers).toEqual([])
+ })
+
+ test("enter advances past the custom row in multi-select", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Tags?",
+ header: "Tags",
+ options: [{ label: "bug", description: "Bug fix" }],
+ multiple: true,
+ custom: true,
+ },
+ ],
+ })
+
+ let state = questionSetSelected(createQuestionBodyState("question-1"), 1)
+ state = questionSelect(state, ask).state
+ expect(state.editing).toBe(false)
+ expect(state.tab).toBe(1)
+ })
+
+ test("enter on custom row in single-select opens the editor", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Mode?",
+ header: "Mode",
+ options: [{ label: "chunked", description: "" }],
+ multiple: false,
+ custom: true,
+ },
+ ],
+ })
+
+ const state = questionSetSelected(createQuestionBodyState("question-1"), 1)
+ const next = questionSelect(state, ask).state
+ expect(next.editing).toBe(true)
+ })
+
+ test("space on custom row with typed value toggles it", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Tags?",
+ header: "Tags",
+ options: [{ label: "bug", description: "Bug fix" }],
+ multiple: true,
+ custom: true,
+ },
+ ],
+ })
+
+ let state = createQuestionBodyState("question-1")
+ state = questionStoreCustom(state, 0, "custom-tag")
+ state = questionSave(state, ask).state
+ expect(state.answers[0]).toContain("custom-tag")
+
+ state = questionSetSelected(state, 1)
+ state = questionToggleAction(state, ask).state
+ expect(state.answers[0]).not.toContain("custom-tag")
+
+ // Toggle it back on
+ state = questionToggleAction(state, ask).state
+ expect(state.answers[0]).toContain("custom-tag")
+ })
+
+ test("space on custom row without typed value opens the editor", () => {
+ const ask = req({
+ questions: [
+ {
+ question: "Tags?",
+ header: "Tags",
+ options: [{ label: "bug", description: "Bug fix" }],
+ multiple: true,
+ custom: true,
+ },
+ ],
+ })
+
+ const state = questionSetSelected(createQuestionBodyState("question-1"), 1)
+ const next = questionToggleAction(state, ask).state
+ expect(next.editing).toBe(true)
+ })
+ // kilocode_change end
+
test("stores and submits custom answers", () => {
let state = questionSetSelected(createQuestionBodyState("question-1"), 1)
let next = questionSelect(state, req())