mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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.
This commit is contained in:
@@ -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.
|
||||
@@ -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() {
|
||||
<Show when={permissions().length === 0 && !question()}>
|
||||
{/* kilocode_change end */}
|
||||
{/* kilocode_change start */}
|
||||
<Show when={suggestion()} keyed>
|
||||
{(request) => (
|
||||
<SuggestPrompt
|
||||
request={request}
|
||||
nonBlocking={request.blocking === false}
|
||||
inputFocused={() => prompt?.focused ?? false}
|
||||
/>
|
||||
)}
|
||||
<Show when={blockingSuggestion()} keyed>
|
||||
{(request) => <SuggestPrompt request={request} />}
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={session()?.parentID}>
|
||||
@@ -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
|
||||
</Match>
|
||||
{/* kilocode_change start */}
|
||||
<Match when={props.part.tool === "suggest"}>
|
||||
<Suggest {...toolprops} InlineTool={InlineTool} BlockTool={BlockTool} />
|
||||
{(() => {
|
||||
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 (
|
||||
<Suggest
|
||||
{...toolprops}
|
||||
InlineTool={InlineTool}
|
||||
BlockTool={BlockTool}
|
||||
pendingRequest={pending()}
|
||||
inputFocused={() => promptRef.current?.focused ?? false}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</Match>
|
||||
{/* kilocode_change end */}
|
||||
<Match when={props.part.tool === "skill"}>
|
||||
|
||||
@@ -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 (
|
||||
<box
|
||||
marginTop={1}
|
||||
paddingLeft={2}
|
||||
paddingTop={0}
|
||||
paddingBottom={0}
|
||||
gap={1}
|
||||
border={["left"]}
|
||||
borderColor={theme.secondary}
|
||||
>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>{props.request.text}</text>
|
||||
</box>
|
||||
<box flexDirection="row" gap={2} paddingLeft={1}>
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const active = () => i() === selected()
|
||||
return (
|
||||
<box
|
||||
flexDirection="row"
|
||||
onMouseOver={() => setSelected(i())}
|
||||
onMouseDown={() => setSelected(i())}
|
||||
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>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<text fg={theme.textMuted}>
|
||||
<Show when={busy()} fallback={<>esc dismiss</>}>
|
||||
Waiting...
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<Show
|
||||
when={props.nonBlocking && props.inputFocused?.()}
|
||||
fallback={
|
||||
<>
|
||||
<text fg={theme.text}>
|
||||
{"↑↓"} <span style={{ fg: theme.textMuted }}>select</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>choose</span>
|
||||
</text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
click <span style={{ fg: theme.textMuted }}>choose</span>
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
{"↑↓"} <span style={{ fg: theme.textMuted }}>select</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>choose</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>dismiss</span>
|
||||
</text>
|
||||
|
||||
@@ -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 <SuggestBar request={props.pendingRequest} inputFocused={props.inputFocused} />
|
||||
}
|
||||
|
||||
return props.InlineTool({
|
||||
icon: "→",
|
||||
pending: "Suggesting next step...",
|
||||
|
||||
Reference in New Issue
Block a user