From 8d06a083bce0d87ad55adeb57b043cc5607979eb Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Wed, 22 Apr 2026 09:59:02 +0300 Subject: [PATCH] tui(cli): render non-blocking suggestions inline in the conversation Moves the 'Run review?' suggestion picker out of the footer bar above the prompt and into the conversation itself, at the position of the suggest tool call. Frees up vertical space for reading while scrolling and matches where the VS Code extension shows the same picker. Clicking an option still accepts, digit keys 1/2 fast-accept when the prompt isn't focused, and Esc dismisses. Blocking suggestions keep the above-prompt overlay. --- .changeset/cli-suggest-inline.md | 5 + .../src/cli/cmd/tui/routes/session/index.tsx | 37 ++++-- .../src/kilocode/suggestion/tui/bar.tsx | 110 ++++++++++++++++++ .../src/kilocode/suggestion/tui/prompt.tsx | 38 ++---- .../src/kilocode/suggestion/tui/render.tsx | 9 +- 5 files changed, 161 insertions(+), 38 deletions(-) create mode 100644 .changeset/cli-suggest-inline.md create mode 100644 packages/opencode/src/kilocode/suggestion/tui/bar.tsx diff --git a/.changeset/cli-suggest-inline.md b/.changeset/cli-suggest-inline.md new file mode 100644 index 00000000000..3e33ed01f6b --- /dev/null +++ b/.changeset/cli-suggest-inline.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +CLI suggestions now render inline in the conversation at the position of the suggest tool call, instead of as a separate bar above the prompt input. This matches the VS Code extension and leaves more room for reading while scrolling. Blocking suggestions still use the above-prompt overlay. diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 8460a87bdf1..ac7435a187e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -159,8 +159,9 @@ export function Session() { const nonBlockingQuestions = createMemo(() => questions().filter((q) => q.blocking === false)) const question = createMemo(() => blockingQuestions()[0] ?? nonBlockingQuestions()[0]) const blockingSuggestions = createMemo(() => suggestions().filter((s) => s.blocking !== false)) - const nonBlockingSuggestions = createMemo(() => suggestions().filter((s) => s.blocking === false)) - const suggestion = createMemo(() => blockingSuggestions()[0] ?? nonBlockingSuggestions()[0]) + // Footer overlay only hosts blocking suggestions now; non-blocking ones + // render inline at the tool-part slot via `SuggestBar`. + const blockingSuggestion = createMemo(() => blockingSuggestions()[0]) const visible = createMemo( () => !session()?.parentID && @@ -1285,14 +1286,8 @@ export function Session() { {/* kilocode_change end */} {/* kilocode_change start */} - - {(request) => ( - prompt?.focused ?? false} - /> - )} + + {(request) => } @@ -1640,6 +1635,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) { const ctx = use() const sync = useSync() + const promptRef = usePromptRef() // kilocode_change - for suggest tool inline bar // Hide tool if showDetails is false and tool completed successfully const shouldHide = createMemo(() => { @@ -1718,7 +1714,26 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess {/* kilocode_change start */} - + {(() => { + const pending = createMemo(() => { + const requests = sync.data.suggestion[props.message.sessionID] ?? [] + return requests.find( + (r) => + r.blocking === false && + r.tool?.callID === props.part.callID && + r.tool?.messageID === props.part.messageID, + ) + }) + return ( + promptRef.current?.focused ?? false} + /> + ) + })()} {/* kilocode_change end */} diff --git a/packages/opencode/src/kilocode/suggestion/tui/bar.tsx b/packages/opencode/src/kilocode/suggestion/tui/bar.tsx new file mode 100644 index 00000000000..7b8bb193097 --- /dev/null +++ b/packages/opencode/src/kilocode/suggestion/tui/bar.tsx @@ -0,0 +1,110 @@ +/** @jsxImportSource @opentui/solid */ +// kilocode_change - new file + +import { useKeyboard } from "@opentui/solid" +import type { SuggestionRequest } from "@kilocode/sdk/v2" +import { createMemo, createSignal, For, Show } from "solid-js" +import { useKeybind } from "../../../cli/cmd/tui/context/keybind" +import { useSDK } from "../../../cli/cmd/tui/context/sdk" +import { tint, useTheme } from "../../../cli/cmd/tui/context/theme" +import { useDialog } from "../../../cli/cmd/tui/ui/dialog" + +export function SuggestBar(props: { request: SuggestionRequest; inputFocused?: () => boolean }) { + const sdk = useSDK() + const { theme } = useTheme() + const keybind = useKeybind() + const dialog = useDialog() + + const options = createMemo(() => props.request.actions) + const [selected, setSelected] = createSignal(0) + const [busy, setBusy] = createSignal(false) + + function accept(index: number) { + if (busy()) return + setBusy(true) + sdk.client.suggestion + .accept({ + requestID: props.request.id, + index, + }) + .catch(() => setBusy(false)) + } + + function reject() { + if (busy()) return + setBusy(true) + sdk.client.suggestion + .dismiss({ + requestID: props.request.id, + }) + .catch(() => setBusy(false)) + } + + useKeyboard((evt) => { + if (dialog.stack.length > 0) return + if (evt.defaultPrevented) return + + if (evt.name === "escape" || keybind.match("app_exit", evt)) { + evt.preventDefault() + reject() + return + } + + // Skip digit shortcuts when the prompt has focus so typed digits go into + // the prompt rather than triggering the bar. Matches the non-blocking + // suppression pattern from the old footer overlay. + if (props.inputFocused?.()) return + + const max = Math.min(options().length, 2) + const digit = Number(evt.name) + if (!Number.isNaN(digit) && digit >= 1 && digit <= max) { + evt.preventDefault() + accept(digit - 1) + } + }) + + return ( + + + {props.request.text} + + + + {(opt, i) => { + const active = () => i() === selected() + return ( + setSelected(i())} + onMouseDown={() => setSelected(i())} + onMouseUp={() => accept(i())} + > + + + {`${i() + 1}.`} + + + + {opt.label} + + + ) + }} + + + esc dismiss}> + Waiting... + + + + + ) +} diff --git a/packages/opencode/src/kilocode/suggestion/tui/prompt.tsx b/packages/opencode/src/kilocode/suggestion/tui/prompt.tsx index de27714d502..21979a23d57 100644 --- a/packages/opencode/src/kilocode/suggestion/tui/prompt.tsx +++ b/packages/opencode/src/kilocode/suggestion/tui/prompt.tsx @@ -2,18 +2,18 @@ import { useKeyboard } from "@opentui/solid" import type { SuggestionRequest } from "@kilocode/sdk/v2" -import { createMemo, createSignal, For, Show } from "solid-js" +import { createMemo, createSignal, For } from "solid-js" import { SplitBorder } from "../../../cli/cmd/tui/component/border" import { useKeybind } from "../../../cli/cmd/tui/context/keybind" import { useSDK } from "../../../cli/cmd/tui/context/sdk" import { tint, useTheme } from "../../../cli/cmd/tui/context/theme" import { useDialog } from "../../../cli/cmd/tui/ui/dialog" -export function SuggestPrompt(props: { - request: SuggestionRequest - nonBlocking?: boolean - inputFocused?: () => boolean -}) { +// The footer-mounted overlay only ever hosts blocking suggestions now; the +// built-in suggest tool emits non-blocking requests that render inline at +// the tool-part slot via `SuggestBar`. See `./bar.tsx` and the dispatch in +// `cli/cmd/tui/routes/session/index.tsx`. +export function SuggestPrompt(props: { request: SuggestionRequest }) { const sdk = useSDK() const { theme } = useTheme() const keybind = useKeybind() @@ -55,7 +55,6 @@ export function SuggestPrompt(props: { useKeyboard((evt) => { if (dialog.stack.length > 0) return if (evt.defaultPrevented) return - const suppressed = props.nonBlocking && props.inputFocused?.() if (evt.name === "escape") { evt.preventDefault() @@ -63,8 +62,6 @@ export function SuggestPrompt(props: { return } - if (suppressed) return - const total = options().length const max = Math.min(total, 9) const digit = Number(evt.name) @@ -155,23 +152,12 @@ export function SuggestPrompt(props: { justifyContent="space-between" > - - - {"↑↓"} select - - - enter choose - - - } - > - - click choose - - + + {"↑↓"} select + + + enter choose + esc dismiss diff --git a/packages/opencode/src/kilocode/suggestion/tui/render.tsx b/packages/opencode/src/kilocode/suggestion/tui/render.tsx index 638794f15d1..4a968e5c6ff 100644 --- a/packages/opencode/src/kilocode/suggestion/tui/render.tsx +++ b/packages/opencode/src/kilocode/suggestion/tui/render.tsx @@ -2,7 +2,8 @@ import { createMemo, Show, type JSX } from "solid-js" import { useTheme } from "../../../cli/cmd/tui/context/theme" -import type { ToolPart as MessageToolPart } from "@kilocode/sdk/v2" +import type { SuggestionRequest, ToolPart as MessageToolPart } from "@kilocode/sdk/v2" +import { SuggestBar } from "./bar" type InlineProps = { icon: string @@ -31,6 +32,8 @@ export function Suggest(props: { part: MessageToolPart InlineTool: (props: InlineProps) => JSX.Element BlockTool: (props: BlockProps) => JSX.Element + pendingRequest?: SuggestionRequest + inputFocused?: () => boolean }) { const { theme } = useTheme() const accepted = createMemo(() => props.metadata.accepted) @@ -54,6 +57,10 @@ export function Suggest(props: { }) } + if (props.pendingRequest) { + return + } + return props.InlineTool({ icon: "→", pending: "Suggesting next step...",