tui(cli): redesign inline suggest bar to match VS Code single-row layout

Replaces the number-prefixed question-picker block with a full-width tinted
row: icon + suggestion text on the left, clickable action buttons on the
right. Removes keyboard shortcuts and the esc-dismiss hint -- dismissal
already happens automatically server-side when the user sends a new prompt.
This commit is contained in:
Alex Alecu
2026-04-22 14:24:56 +03:00
parent e7bcc93ebb
commit 40cb8889fe
4 changed files with 37 additions and 88 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"@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.
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. The inline bar renders as a single full-width row with a subtle background and clickable action buttons, matching the VS Code extension. Dismissal happens automatically when you send a new prompt. Blocking suggestions still use the above-prompt overlay.
@@ -23,15 +23,7 @@ import { Spinner } from "@tui/component/spinner"
import { selectedForeground, useTheme } from "@tui/context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { Prompt, type PromptRef } from "@tui/component/prompt"
import type {
AssistantMessage,
Part,
Provider,
ToolPart,
UserMessage,
TextPart,
ReasoningPart,
} from "@kilocode/sdk/v2"
import type { AssistantMessage, Part, Provider, ToolPart, UserMessage, TextPart, ReasoningPart } from "@kilocode/sdk/v2"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool"
@@ -1635,7 +1627,6 @@ 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(() => {
@@ -1724,15 +1715,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
r.tool?.messageID === props.part.messageID,
)
})
return (
<Suggest
{...toolprops}
InlineTool={InlineTool}
BlockTool={BlockTool}
pendingRequest={pending()}
inputFocused={() => promptRef.current?.focused ?? false}
/>
)
return <Suggest {...toolprops} InlineTool={InlineTool} BlockTool={BlockTool} pendingRequest={pending()} />
})()}
</Match>
{/* kilocode_change end */}
@@ -1,22 +1,16 @@
/** @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 { createMemo, createSignal, For } from "solid-js"
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"
import { selectedForeground, useTheme } from "../../../cli/cmd/tui/context/theme"
export function SuggestBar(props: { request: SuggestionRequest; inputFocused?: () => boolean }) {
export function SuggestBar(props: { request: SuggestionRequest }) {
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) {
@@ -30,80 +24,53 @@ export function SuggestBar(props: { request: SuggestionRequest; inputFocused?: (
.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 (
<box
marginTop={1}
backgroundColor={theme.backgroundElement}
flexDirection="row"
justifyContent="space-between"
alignItems="center"
paddingLeft={2}
paddingTop={0}
paddingBottom={0}
gap={1}
border={["left"]}
borderColor={theme.secondary}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
flexShrink={0}
>
<box paddingLeft={1}>
<box flexDirection="row" gap={1} flexShrink={1}>
<text fg={theme.secondary}></text>
<text fg={theme.text}>{props.request.text}</text>
</box>
<box flexDirection="row" gap={2} paddingLeft={1}>
<box flexDirection="row" gap={1} flexShrink={0}>
<For each={options()}>
{(opt, i) => {
const active = () => i() === selected()
const [hover, setHover] = createSignal(false)
const primary = () => i() === 0
const bg = () => {
if (busy()) return undefined
if (hover()) return theme.accent
if (primary()) return theme.backgroundPanel
return undefined
}
const fg = () => {
if (busy()) return theme.textMuted
if (hover()) return selectedForeground(theme, theme.accent)
return theme.text
}
return (
<box
flexDirection="row"
onMouseOver={() => setSelected(i())}
onMouseDown={() => setSelected(i())}
paddingLeft={2}
paddingRight={2}
backgroundColor={bg()}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => accept(i())}
>
<box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
<text fg={active() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
{`${i() + 1}.`}
</text>
</box>
<box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
<text fg={active() ? theme.secondary : theme.text}>{opt.label}</text>
</box>
<text fg={fg()}>{opt.label}</text>
</box>
)
}}
</For>
<text fg={theme.textMuted}>
<Show when={busy()} fallback={<>esc dismiss</>}>
Waiting...
</Show>
</text>
</box>
</box>
)
@@ -33,7 +33,6 @@ export function Suggest(props: {
InlineTool: (props: InlineProps) => JSX.Element
BlockTool: (props: BlockProps) => JSX.Element
pendingRequest?: SuggestionRequest
inputFocused?: () => boolean
}) {
const { theme } = useTheme()
const accepted = createMemo(() => props.metadata.accepted)
@@ -62,7 +61,7 @@ export function Suggest(props: {
}
</Match>
<Match when={props.pendingRequest} keyed>
{(request) => <SuggestBar request={request} inputFocused={props.inputFocused} />}
{(request) => <SuggestBar request={request} />}
</Match>
<Match when={true}>
{(_) =>