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...",