revert: remove suggest tool feature (#8994)

Reverts PR #6404 (feat: suggest code review tool) and PR #8988 (fix: dismiss suggestions on new message).
This commit is contained in:
Marius
2026-04-15 16:13:42 +00:00
committed by GitHub
parent ae8e7b6e1c
commit d4ab3331c8
60 changed files with 50 additions and 2563 deletions
-3
View File
@@ -85,7 +85,6 @@ import {
handleQuestionReject,
fetchAndSendPendingQuestions,
} from "./kilo-provider/handlers/question"
import { fetchAndSendPendingSuggestions, routeSuggestionWebviewMessage } from "./kilo-provider/handlers/suggestion"
import {
buildActionContext,
@@ -507,7 +506,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await Promise.all([
fetchAndSendPendingPermissions(this.permissionCtx),
fetchAndSendPendingQuestions(this.questionCtx),
fetchAndSendPendingSuggestions(this.questionCtx),
])
}
}
@@ -561,7 +559,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
await routeSuggestionWebviewMessage(this.questionCtx, message)
switch (message.type) {
case "webviewReady":
console.log("[Kilo New] KiloProvider: ✅ webviewReady received")
@@ -307,24 +307,8 @@ export type WebviewMessage =
}
}
| { type: "todoUpdated"; sessionID: string; items: unknown[] }
| {
type: "questionRequest"
question: { id: string; sessionID: string; questions: unknown[]; blocking?: boolean; tool?: unknown }
}
| { type: "questionRequest"; question: { id: string; sessionID: string; questions: unknown[]; tool?: unknown } }
| { type: "questionResolved"; requestID: string }
| {
type: "suggestionRequest"
suggestion: {
id: string
sessionID: string
text: string
actions: unknown[]
blocking?: boolean
tool?: unknown
}
}
| { type: "suggestionResolved"; requestID: string }
| { type: "suggestionError"; requestID: string }
| { type: "permissionResolved"; permissionID: string }
| { type: "permissionError"; permissionID: string }
| { type: "sessionCreated"; session: ReturnType<typeof sessionToWebview>; draftID?: string }
@@ -428,7 +412,6 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
id: event.properties.id,
sessionID: event.properties.sessionID,
questions: event.properties.questions,
blocking: event.properties.blocking,
tool: event.properties.tool,
},
}
@@ -438,24 +421,6 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un
type: "questionResolved",
requestID: event.properties.requestID,
}
case "suggestion.shown":
return {
type: "suggestionRequest",
suggestion: {
id: event.properties.id,
sessionID: event.properties.sessionID,
text: event.properties.text,
actions: event.properties.actions,
blocking: event.properties.blocking,
tool: event.properties.tool,
},
}
case "suggestion.accepted":
case "suggestion.dismissed":
return {
type: "suggestionResolved",
requestID: event.properties.requestID,
}
case "session.error": {
return {
type: "sessionError",
@@ -41,7 +41,6 @@ export async function fetchAndSendPendingQuestions(ctx: QuestionContext): Promis
id: q.id,
sessionID: q.sessionID,
questions: q.questions,
blocking: q.blocking,
tool: q.tool,
},
})
@@ -1,110 +0,0 @@
/**
* Suggestion handlers — extracted from KiloProvider.
*
* Manages suggestion accept and dismiss flows plus recovery after SSE reconnects.
* No vscode dependency.
*/
import type { KiloClient, SuggestionRequest } from "@kilocode/sdk/v2/client"
import { recoveryDirs } from "./permission-handler"
export type RecoverableSuggestion = SuggestionRequest
export interface SuggestionContext {
readonly client: KiloClient | null
readonly currentSessionId: string | undefined
readonly trackedSessionIds: Set<string>
readonly sessionDirectories: ReadonlyMap<string, string>
postMessage(msg: unknown): void
getWorkspaceDirectory(sessionId?: string): string
}
export function recoverableSuggestions(items: RecoverableSuggestion[], tracked: Set<string>, seen: Set<string>) {
return items.filter((item) => {
if (seen.has(item.id)) return false
seen.add(item.id)
return tracked.has(item.sessionID)
})
}
/**
* Route suggestion-related webview messages.
* Extracted from the main message handler to stay within the complexity limit.
*/
export async function routeSuggestionWebviewMessage(
ctx: SuggestionContext,
message: { type: string; requestID?: string; sessionID?: string; index?: number },
): Promise<void> {
switch (message.type) {
case "suggestionAccept":
await handleSuggestionAccept(ctx, message.requestID!, message.index!, message.sessionID)
break
case "suggestionDismiss":
await handleSuggestionDismiss(ctx, message.requestID!, message.sessionID)
break
}
}
export async function handleSuggestionAccept(
ctx: SuggestionContext,
requestID: string,
index: number,
sessionID?: string,
): Promise<void> {
if (!ctx.client) {
ctx.postMessage({ type: "suggestionError", requestID })
return
}
try {
await ctx.client.suggestion.accept(
{ requestID, index, directory: ctx.getWorkspaceDirectory(sessionID ?? ctx.currentSessionId) },
{ throwOnError: true },
)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to accept suggestion:", error)
ctx.postMessage({ type: "suggestionError", requestID })
}
}
export async function handleSuggestionDismiss(
ctx: SuggestionContext,
requestID: string,
sessionID?: string,
): Promise<void> {
if (!ctx.client) {
ctx.postMessage({ type: "suggestionError", requestID })
return
}
try {
await ctx.client.suggestion.dismiss(
{ requestID, directory: ctx.getWorkspaceDirectory(sessionID ?? ctx.currentSessionId) },
{ throwOnError: true },
)
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to dismiss suggestion:", error)
ctx.postMessage({ type: "suggestionError", requestID })
}
}
export async function fetchAndSendPendingSuggestions(ctx: SuggestionContext): Promise<void> {
if (!ctx.client) return
try {
const dirs = recoveryDirs(ctx.getWorkspaceDirectory(), ctx.sessionDirectories)
const seen = new Set<string>()
for (const dir of dirs) {
const { data } = await ctx.client.suggestion.list({ directory: dir })
if (!data) continue
for (const suggestion of recoverableSuggestions(data, ctx.trackedSessionIds, seen)) {
ctx.postMessage({
type: "suggestionRequest",
suggestion,
})
}
}
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to fetch pending suggestions:", error)
}
}
@@ -405,7 +405,6 @@ export class KiloConnectionService {
if (error) throw new Error(`Failed to reject question ${q.id}: ${String(error)}`)
}
}
await drainSuggestions(this.client, dir)
await drainNetworkWaits(this.client, dir)
}
for (const listener of this.clearPendingPromptsListeners) {
@@ -634,14 +633,3 @@ export class KiloConnectionService {
this.startHealthPoll(config.baseUrl, config.password)
}
}
async function drainSuggestions(client: KiloClient, directory: string): Promise<void> {
const { data, error: err } = await client.suggestion.list({ directory })
if (err) throw new Error(`Failed to list suggestions for ${directory}: ${String(err)}`)
if (data) {
for (const s of data) {
const { error } = await client.suggestion.dismiss({ requestID: s.id, directory })
if (error) throw new Error(`Failed to dismiss suggestion ${s.id}: ${String(error)}`)
}
}
}
@@ -41,17 +41,6 @@ export function resolveEventSessionId(
case "question.replied":
case "question.rejected":
return event.properties.sessionID
default:
return resolveSuggestionSessionId(event)
}
}
function resolveSuggestionSessionId(event: Event): string | undefined {
switch (event.type) {
case "suggestion.shown":
case "suggestion.accepted":
case "suggestion.dismissed":
return event.properties.sessionID
default:
// session.network.* events are not yet in the SDK Event type union
// (pending SDK regeneration). Handle them via string comparison.
@@ -149,30 +149,6 @@ describe("resolveEventSessionId", () => {
expect(resolveEventSessionId(e, noLookup)).toBe("s11")
})
it("returns sessionID from suggestion.shown", () => {
const e = event({
type: "suggestion.shown",
properties: { id: "sug_1", sessionID: "s12", text: "Review?", actions: [] },
})
expect(resolveEventSessionId(e, noLookup)).toBe("s12")
})
it("returns sessionID from suggestion.accepted", () => {
const e = event({
type: "suggestion.accepted",
properties: { sessionID: "s13", requestID: "sug_1", index: 0, action: { label: "Start", prompt: "x" } },
})
expect(resolveEventSessionId(e, noLookup)).toBe("s13")
})
it("returns sessionID from suggestion.dismissed", () => {
const e = event({
type: "suggestion.dismissed",
properties: { sessionID: "s14", requestID: "sug_2" },
})
expect(resolveEventSessionId(e, noLookup)).toBe("s14")
})
it("returns undefined for unknown event types (global events)", () => {
const e = event({ type: "server.connected", properties: {} })
expect(resolveEventSessionId(e, noLookup)).toBeUndefined()
@@ -26,9 +26,6 @@ import type {
EventQuestionAsked,
EventQuestionReplied,
EventQuestionRejected,
EventSuggestionShown,
EventSuggestionAccepted,
EventSuggestionDismissed,
EventSessionCreated,
EventSessionUpdated,
EventServerConnected,
@@ -447,43 +444,6 @@ describe("mapSSEEventToWebviewMessage", () => {
}
})
it("maps suggestion.shown to suggestionRequest", () => {
const event: EventSuggestionShown = {
type: "suggestion.shown",
properties: {
id: "sug-1",
sessionID: "sess-1",
text: "Review changes?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
},
}
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
expect(msg?.type).toBe("suggestionRequest")
})
it("maps suggestion.accepted to suggestionResolved", () => {
const event: EventSuggestionAccepted = {
type: "suggestion.accepted",
properties: {
sessionID: "sess-1",
requestID: "sug-1",
index: 0,
action: { label: "Start", prompt: "/local-review-uncommitted" },
},
}
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
expect(msg?.type).toBe("suggestionResolved")
})
it("maps suggestion.dismissed to suggestionResolved", () => {
const event: EventSuggestionDismissed = {
type: "suggestion.dismissed",
properties: { sessionID: "sess-1", requestID: "sug-2" },
}
const msg = mapSSEEventToWebviewMessage(event, "sess-1")
expect(msg?.type).toBe("suggestionResolved")
})
it("maps session.created to sessionCreated with ISO dates", () => {
const event: EventSessionCreated = {
type: "session.created",
@@ -1,89 +0,0 @@
import { describe, expect, it } from "bun:test"
import {
fetchAndSendPendingSuggestions,
recoverableSuggestions,
type RecoverableSuggestion,
type SuggestionContext,
} from "../../src/kilo-provider/handlers/suggestion"
function pending(id: string, sessionID: string): RecoverableSuggestion {
return {
id,
sessionID,
text: "Review changes?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
}
}
type Items = Record<string, RecoverableSuggestion[]>
function suggestionClient(itemsPerDir: Items, queries: string[]) {
return {
suggestion: {
list: async (args?: { directory?: string }) => {
const dir = args?.directory ?? ""
queries.push(dir)
return { data: itemsPerDir[dir] ?? [] }
},
accept: async () => ({ data: true }),
dismiss: async () => ({ data: true }),
},
}
}
function ctx(opts: { tracked: string[]; dirs?: Map<string, string>; itemsPerDir?: Items }) {
const messages: unknown[] = []
const queries: string[] = []
const sdk = suggestionClient(opts.itemsPerDir ?? {}, queries) as unknown as SuggestionContext["client"]
const fake: SuggestionContext = {
client: sdk,
currentSessionId: undefined,
trackedSessionIds: new Set(opts.tracked),
sessionDirectories: opts.dirs ?? new Map(),
postMessage: (msg) => messages.push(msg),
getWorkspaceDirectory: () => "/workspace",
}
return { fake, messages, queries }
}
describe("recoverableSuggestions", () => {
it("filters out untracked suggestions and deduplicates by id", () => {
const seen = new Set<string>()
const list = [pending("s1", "tracked"), pending("s1", "tracked"), pending("s2", "other")]
expect(recoverableSuggestions(list, new Set(["tracked"]), seen)).toEqual([pending("s1", "tracked")])
})
})
describe("fetchAndSendPendingSuggestions", () => {
it("forwards suggestions from tracked sessions", async () => {
const dirs = new Map([["s1", "/wt"]])
const { fake, messages, queries } = ctx({
tracked: ["s1"],
dirs,
itemsPerDir: { "/wt": [pending("sug-1", "s1")] },
})
await fetchAndSendPendingSuggestions(fake)
expect(queries).toContain("/workspace")
expect(queries).toContain("/wt")
expect(messages).toEqual([{ type: "suggestionRequest", suggestion: pending("sug-1", "s1") }])
})
it("does nothing when client is null", async () => {
const messages: unknown[] = []
const fake: SuggestionContext = {
client: null,
currentSessionId: undefined,
trackedSessionIds: new Set(["s1"]),
sessionDirectories: new Map(),
postMessage: (msg) => messages.push(msg),
getWorkspaceDirectory: () => "/workspace",
}
await fetchAndSendPendingSuggestions(fake)
expect(messages).toHaveLength(0)
})
})
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eee5f5d61cf2ac9ad0b987a8b220d76c503be0d11447766e6669da4684698c82
size 6130
@@ -19,7 +19,6 @@ import type {
import { useData } from "@kilocode/kilo-ui/context/data"
import { useSession } from "../../context/session"
import { QuestionDock } from "./QuestionDock"
import { SuggestBar } from "./SuggestBar"
// Tools that the upstream message-part renderer suppresses (returns null for).
// We render these ourselves via ToolRegistry when they complete,
@@ -42,22 +41,6 @@ function isRenderable(part: SDKPart): boolean {
return !!PART_MAPPING[part.type]
}
/**
* Match a tool part to an active request (question or suggestion) by tool name
* and callID/messageID. Returns the matched request or undefined.
*/
function matchToolRequest<T extends { tool?: { callID: string; messageID: string } }>(
part: SDKPart,
name: string,
requests: T[],
): T | undefined {
if (part.type !== "tool") return undefined
const tp = part as unknown as ToolPart
if (tp.tool !== name) return undefined
if (tp.state?.status !== "pending" && tp.state?.status !== "running") return undefined
return requests.find((r) => r.tool?.callID === tp.callID && r.tool?.messageID === tp.messageID)
}
interface AssistantMessageProps {
message: SDKAssistantMessage
showAssistantCopyPartID?: string | null
@@ -104,40 +87,36 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
part.type === "tool" && UPSTREAM_SUPPRESSED_TOOLS.has((part as SDKPart & { tool: string }).tool)
// Active question tool parts render the interactive QuestionDock inline
const activeQuestion = createMemo(() => matchToolRequest(part, "question", session.questions()))
// Active suggestion tool parts render the interactive SuggestBar inline
const activeSuggestion = createMemo(() => matchToolRequest(part, "suggest", session.suggestions()))
const activeQuestion = createMemo(() => {
if (part.type !== "tool") return undefined
const tp = part as unknown as ToolPart
if (tp.tool !== "question") return undefined
if (tp.state?.status !== "pending" && tp.state?.status !== "running") return undefined
return session.questions().find((q) => q.tool?.callID === tp.callID && q.tool?.messageID === tp.messageID)
})
return (
<Show when={isUpstreamSuppressed || activeQuestion() || activeSuggestion() || PART_MAPPING[part.type]}>
<Show when={isUpstreamSuppressed || activeQuestion() || PART_MAPPING[part.type]}>
<div data-component="tool-part-wrapper" data-part-type={part.type}>
<Show
when={activeQuestion()}
fallback={
<Show
when={activeSuggestion()}
when={isUpstreamSuppressed}
fallback={
<Show
when={isUpstreamSuppressed}
fallback={
<Part
part={part}
message={props.message as SDKMessage}
showAssistantCopyPartID={props.showAssistantCopyPartID}
animate={
part.type === "tool" &&
((part as unknown as ToolPart).state?.status === "pending" ||
(part as unknown as ToolPart).state?.status === "running")
}
/>
<Part
part={part}
message={props.message as SDKMessage}
showAssistantCopyPartID={props.showAssistantCopyPartID}
animate={
part.type === "tool" &&
((part as unknown as ToolPart).state?.status === "pending" ||
(part as unknown as ToolPart).state?.status === "running")
}
>
<TodoToolCard part={part as unknown as ToolPart} />
</Show>
/>
}
>
{(req) => <SuggestBar request={req()} />}
<TodoToolCard part={part as unknown as ToolPart} />
</Show>
}
>
@@ -55,16 +55,13 @@ export const ChatView: Component<ChatViewProps> = (props) => {
// not once per accessor call (questionRequest, permissionRequest, blocked all read these).
const familyPermissions = createMemo(() => session.scopedPermissions(id()))
const familyQuestions = createMemo(() => session.scopedQuestions(id()))
const familySuggestions = createMemo(() => session.scopedSuggestions(id()))
// Non-tool questions (standalone, not from the question tool) render inline in
// the message list since they don't have an associated tool part in the conversation.
// Tool-linked questions render inline at their tool part position via AssistantMessage.
const standaloneQuestions = createMemo(() => familyQuestions().filter((q) => !q.tool))
const standaloneSuggestions = createMemo(() => familySuggestions().filter((s) => !s.tool))
const permissionRequest = () => familyPermissions().find((p) => p.sessionID === id()) ?? familyPermissions()[0]
const blocked = () => familyPermissions().length > 0 || familyQuestions().some((q) => q.blocking !== false)
// Session is busy only because a suggestion tool call is pending — prompt should behave as idle
const suggesting = () => !blocked() && familySuggestions().length > 0
const blocked = () => familyPermissions().length > 0 || familyQuestions().length > 0
const dock = () => !props.readonly || !!permissionRequest()
// When a bottom-dock permission disappears while the session is busy,
@@ -133,7 +130,6 @@ export const ChatView: Component<ChatViewProps> = (props) => {
onSelectSession={props.onSelectSession}
onShowHistory={props.onShowHistory}
questions={standaloneQuestions}
suggestions={standaloneSuggestions}
readonly={props.readonly}
/>
</div>
@@ -219,12 +215,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
</div>
</Show>
<Show when={!props.readonly}>
<PromptInput
blocked={blocked}
suggesting={suggesting}
boxId={props.promptBoxId}
pendingSessionID={props.pendingSessionID}
/>
<PromptInput blocked={blocked} boxId={props.promptBoxId} pendingSessionID={props.pendingSessionID} />
</Show>
</div>
</Show>
@@ -23,9 +23,8 @@ import { AccountSwitcher } from "../shared/AccountSwitcher"
import { KiloNotifications } from "./KiloNotifications"
import { WorkingIndicator } from "../shared/WorkingIndicator"
import { QuestionDock } from "./QuestionDock"
import { SuggestBar } from "./SuggestBar"
import { activeUserMessageID as getActiveUserMessageID } from "../../context/session-queue"
import type { QuestionRequest, SuggestionRequest } from "../../types/messages"
import type { QuestionRequest } from "../../types/messages"
const KiloLogo = (): JSX.Element => {
const iconsBaseUri = (window as { ICONS_BASE_URI?: string }).ICONS_BASE_URI || ""
@@ -45,8 +44,6 @@ interface MessageListProps {
onShowHistory?: () => void
/** Non-tool question requests to render inline at the bottom of the message list */
questions?: () => QuestionRequest[]
/** Non-tool suggestion requests to render inline at the bottom of the message list */
suggestions?: () => SuggestionRequest[]
/** When true (subagent viewer), replace the welcome screen with an initializing indicator */
readonly?: boolean
}
@@ -176,7 +173,6 @@ export const MessageList: Component<MessageListProps> = (props) => {
</Show>
<WorkingIndicator />
<For each={props.questions?.()}>{(req) => <QuestionDock request={req} />}</For>
<For each={props.suggestions?.()}>{(req) => <SuggestBar request={req} />}</For>
</Show>
</div>
</div>
@@ -50,8 +50,6 @@ function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]
interface PromptInputProps {
blocked?: () => boolean
/** When true, session is busy only because a suggestion is pending — treat as idle for input */
suggesting?: () => boolean
boxId?: string
pendingSessionID?: string
}
@@ -270,7 +268,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
window.addEventListener("compactSession", onCompact)
onCleanup(() => window.removeEventListener("compactSession", onCompact))
const isBusy = () => session.status() !== "idle" && !props.suggesting?.()
const isBusy = () => session.status() !== "idle"
const isDisabled = () => !server.isConnected()
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
const canSend = () => hasInput() && !isDisabled() && !terminal.pending() && !props.blocked?.()
@@ -1,65 +0,0 @@
import { Button } from "@kilocode/kilo-ui/button"
import { Icon } from "@kilocode/kilo-ui/icon"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import type { Component } from "solid-js"
import { For, Show } from "solid-js"
import { useLanguage } from "../../context/language"
import { useSession } from "../../context/session"
import type { SuggestionRequest } from "../../types/messages"
export const SuggestBar: Component<{ request: SuggestionRequest }> = (props) => {
const session = useSession()
const language = useLanguage()
const error = () => session.suggestionErrors().has(props.request.id)
const responding = () => session.respondingSuggestions().has(props.request.id)
const accept = (index: number) => {
if (responding()) return
session.acceptSuggestion(props.request.id, index)
}
const dismiss = () => {
if (responding()) return
session.dismissSuggestion(props.request.id)
}
return (
<div data-component="suggest-bar">
<div data-slot="suggest-bar-copy">
<span data-slot="suggest-bar-icon">
<Icon name="brain" size="small" />
</span>
<span data-slot="suggest-bar-text">{props.request.text}</span>
</div>
<Show when={error()}>
<div data-slot="suggest-bar-error">
<Icon name="close" size="small" />
<span>Failed try again</span>
</div>
</Show>
<div data-slot="suggest-bar-actions">
<For each={props.request.actions}>
{(action, index) => (
<Button
variant={index() === 0 ? "secondary" : "ghost"}
size="small"
disabled={responding()}
onClick={() => accept(index())}
>
{action.label}
</Button>
)}
</For>
<IconButton
icon="close"
variant="ghost"
size="small"
disabled={responding()}
label={language.t("common.dismiss")}
onClick={dismiss}
/>
</div>
</div>
)
}
@@ -83,8 +83,7 @@ export const WorkingIndicator: Component = () => {
.permissions()
.filter((p) => p.sessionID === id && !(p.tool && ["todowrite", "todoread"].includes(p.toolName)))
const questions = session.questions().filter((q) => q.sessionID === id)
const suggestions = session.suggestions().filter((s) => s.sessionID === id)
return perms.length > 0 || questions.length > 0 || suggestions.length > 0
return perms.length > 0 || questions.length > 0
}
const isRetrying = () => session.statusInfo().type === "retry"
@@ -22,7 +22,6 @@ import type {
SessionStatusInfo,
PermissionRequest,
QuestionRequest,
SuggestionRequest,
TodoItem,
ModelSelection,
ContextUsage,
@@ -114,14 +113,10 @@ interface SessionContextValue {
// Pending question requests (unscoped — all tracked sessions)
questions: Accessor<QuestionRequest[]>
questionErrors: Accessor<Set<string>>
suggestions: Accessor<SuggestionRequest[]>
suggestionErrors: Accessor<Set<string>>
respondingSuggestions: Accessor<Set<string>>
// Scoped permissions/questions — filtered to a session's family (self + subagents)
scopedPermissions: (sessionID: string | undefined) => PermissionRequest[]
scopedQuestions: (sessionID: string | undefined) => QuestionRequest[]
scopedSuggestions: (sessionID: string | undefined) => SuggestionRequest[]
// Model selection (global, extension-lifetime)
selected: Accessor<ModelSelection | null>
@@ -196,8 +191,6 @@ interface SessionContextValue {
) => void
replyToQuestion: (requestID: string, answers: string[][]) => void
rejectQuestion: (requestID: string) => void
acceptSuggestion: (requestID: string, index: number) => void
dismissSuggestion: (requestID: string) => void
createSession: () => void
clearCurrentSession: () => void
loadSessions: () => void
@@ -257,9 +250,6 @@ export const SessionProvider: ParentComponent = (props) => {
// Tracks question IDs that failed so the UI can reset sending state
const [questionErrors, setQuestionErrors] = createSignal<Set<string>>(new Set())
const [suggestions, setSuggestions] = createSignal<SuggestionRequest[]>([])
const [suggestionErrors, setSuggestionErrors] = createSignal<Set<string>>(new Set())
const [respondingSuggestions, setRespondingSuggestions] = createSignal<Set<string>>(new Set())
// Tracks whether the user has explicitly set a model override per agent (to
// prevent the default-sync effect from overwriting it).
@@ -653,9 +643,6 @@ export const SessionProvider: ParentComponent = (props) => {
// Handle messages from extension
onMount(() => {
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
// Route suggestion messages (extracted to stay within complexity limit)
routeSuggestionMessage(message)
switch (message.type) {
case "sessionCreated":
handleSessionCreated(message.session, message.draftID)
@@ -696,10 +683,7 @@ export const SessionProvider: ParentComponent = (props) => {
case "clearPendingPrompts":
setPermissions([])
setQuestions([])
setSuggestions([])
setRespondingPermissions(new Set<string>())
setSuggestionErrors(new Set<string>())
setRespondingSuggestions(new Set<string>())
break
case "sessionsLoaded":
@@ -1016,60 +1000,6 @@ export const SessionProvider: ParentComponent = (props) => {
setQuestionErrors((prev) => new Set(prev).add(requestID))
}
function handleSuggestionRequest(suggestion: SuggestionRequest) {
setSuggestions((prev) => {
const idx = prev.findIndex((item) => item.id === suggestion.id)
if (idx === -1) return [...prev, suggestion]
const next = prev.slice()
next[idx] = suggestion
return next
})
}
function handleSuggestionResolved(requestID: string) {
setSuggestions((prev) => prev.filter((item) => item.id !== requestID))
setRespondingSuggestions((prev) => {
if (!prev.has(requestID)) return prev
const next = new Set(prev)
next.delete(requestID)
return next
})
setSuggestionErrors((prev) => {
if (!prev.has(requestID)) return prev
const next = new Set(prev)
next.delete(requestID)
return next
})
}
function handleSuggestionError(requestID: string) {
setRespondingSuggestions((prev) => {
if (!prev.has(requestID)) return prev
const next = new Set(prev)
next.delete(requestID)
return next
})
setSuggestionErrors((prev) => new Set(prev).add(requestID))
}
/**
* Route suggestion-related extension messages.
* Extracted from the main message handler to stay within the complexity limit.
*/
function routeSuggestionMessage(message: ExtensionMessage) {
switch (message.type) {
case "suggestionRequest":
handleSuggestionRequest(message.suggestion)
break
case "suggestionResolved":
handleSuggestionResolved(message.requestID)
break
case "suggestionError":
handleSuggestionError(message.requestID)
break
}
}
/**
* Handle a failed send: remove the optimistic message from the store
* and show a toast. The PromptInput restores the draft text separately
@@ -1186,12 +1116,6 @@ export const SessionProvider: ParentComponent = (props) => {
return questions().filter((q) => family.has(q.sessionID))
}
function scopedSuggestions(sessionID: string | undefined): SuggestionRequest[] {
if (!sessionID) return []
const family = sessionFamily(sessionID)
return suggestions().filter((item) => family.has(item.sessionID))
}
function handleTodoUpdated(sessionID: string, items: TodoItem[]) {
setStore("todos", sessionID, items)
}
@@ -1272,24 +1196,6 @@ export const SessionProvider: ParentComponent = (props) => {
return next
})
}
const gone = suggestions()
.filter((item) => item.sessionID === sessionID)
.map((item) => item.id)
if (gone.length > 0) {
setSuggestions((prev) => prev.filter((item) => item.sessionID !== sessionID))
setSuggestionErrors((prev) => {
const next = new Set(prev)
for (const id of gone) next.delete(id)
if (next.size === prev.size) return prev
return next
})
setRespondingSuggestions((prev) => {
const next = new Set(prev)
for (const id of gone) next.delete(id)
if (next.size === prev.size) return prev
return next
})
}
setPermissions((prev) => removeSessionPermissions(prev, sessionID))
setStatusMap(
produce((map) => {
@@ -1502,8 +1408,6 @@ export const SessionProvider: ParentComponent = (props) => {
}
const sid = currentSessionID()
const suggestion = scopedSuggestions(sid)[0]
if (suggestion) dismissSuggestion(suggestion.id)
if (sid) addOptimistic(sid, messageID, text, files)
const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined
@@ -1557,8 +1461,6 @@ export const SessionProvider: ParentComponent = (props) => {
const messageID = Identifier.ascending("message")
const sid = currentSessionID()
const suggestion = scopedSuggestions(sid)[0]
if (suggestion) dismissSuggestion(suggestion.id)
if (sid) addOptimistic(sid, messageID, `/${command} ${args}`.trim(), files)
@@ -1646,15 +1548,6 @@ export const SessionProvider: ParentComponent = (props) => {
})
}
function clearSuggestionError(requestID: string) {
setSuggestionErrors((prev) => {
if (!prev.has(requestID)) return prev
const next = new Set(prev)
next.delete(requestID)
return next
})
}
function replyToQuestion(requestID: string, answers: string[][]) {
clearQuestionError(requestID)
const question = questions().find((item) => item.id === requestID)
@@ -1678,29 +1571,6 @@ export const SessionProvider: ParentComponent = (props) => {
})
}
function acceptSuggestion(requestID: string, index: number) {
clearSuggestionError(requestID)
setRespondingSuggestions((prev) => new Set(prev).add(requestID))
const sid = suggestions().find((s) => s.id === requestID)?.sessionID ?? currentSessionID() ?? ""
vscode.postMessage({
type: "suggestionAccept",
requestID,
sessionID: sid,
index,
})
}
function dismissSuggestion(requestID: string) {
clearSuggestionError(requestID)
setRespondingSuggestions((prev) => new Set(prev).add(requestID))
const sid = suggestions().find((s) => s.id === requestID)?.sessionID ?? currentSessionID() ?? ""
vscode.postMessage({
type: "suggestionDismiss",
requestID,
sessionID: sid,
})
}
function createSession() {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot create session: not connected")
@@ -1945,12 +1815,8 @@ export const SessionProvider: ParentComponent = (props) => {
respondingPermissions,
questions,
questionErrors,
suggestions,
suggestionErrors,
respondingSuggestions,
scopedPermissions,
scopedQuestions,
scopedSuggestions,
selected,
selectModel,
hasModelOverride,
@@ -2012,8 +1878,6 @@ export const SessionProvider: ParentComponent = (props) => {
respondToPermission,
replyToQuestion,
rejectQuestion,
acceptSuggestion,
dismissSuggestion,
createSession,
clearCurrentSession,
loadSessions,
@@ -33,13 +33,7 @@ import { dict as appEn } from "../i18n/en"
import { dict as amEn } from "../../agent-manager/i18n/en"
import { dict as kiloEn } from "@kilocode/kilo-i18n/en"
import { resolveTemplate } from "../context/language-utils"
import type {
Config,
KilocodeNotification,
PermissionRequest,
QuestionRequest,
SuggestionRequest,
} from "../types/messages"
import type { Config, KilocodeNotification, PermissionRequest, QuestionRequest } from "../types/messages"
// Merged English dictionary (same merge order as the real LanguageProvider)
const dict: Record<string, string> = { ...appEn, ...amEn, ...uiEn, ...kiloEn }
@@ -126,13 +120,11 @@ export function mockSessionValue(overrides?: {
id?: string
permissions?: PermissionRequest[]
questions?: QuestionRequest[]
suggestions?: SuggestionRequest[]
status?: string
}) {
const id = overrides?.id ?? "story-session-001"
const permissions = overrides?.permissions ?? []
const qs = overrides?.questions ?? []
const suggestions = overrides?.suggestions ?? []
const status = (overrides?.status ?? "idle") as "idle" | "busy"
return {
@@ -162,12 +154,8 @@ export function mockSessionValue(overrides?: {
respondingPermissions: () => new Set<string>(),
questions: () => qs,
questionErrors: () => new Set<string>(),
suggestions: () => suggestions,
suggestionErrors: () => new Set<string>(),
respondingSuggestions: () => new Set<string>(),
scopedPermissions: (sid?: string) => (sid ? permissions.filter((p) => p.sessionID === sid) : permissions),
scopedQuestions: (sid?: string) => (sid ? qs.filter((q) => q.sessionID === sid) : qs),
scopedSuggestions: (sid?: string) => (sid ? suggestions.filter((item) => item.sessionID === sid) : suggestions),
selected: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }),
selectModel: noop,
hasModelOverride: () => false,
@@ -198,14 +186,11 @@ export function mockSessionValue(overrides?: {
currentVariant: () => undefined,
selectVariant: noop,
sendMessage: noop,
sendCommand: noop,
abort: noop,
compact: noop,
respondToPermission: noop,
replyToQuestion: noop,
rejectQuestion: noop,
acceptSuggestion: noop,
dismissSuggestion: noop,
createSession: noop,
clearCurrentSession: noop,
loadSessions: noop,
@@ -226,7 +211,6 @@ interface StoryProvidersProps {
data?: any
permissions?: PermissionRequest[]
questions?: QuestionRequest[]
suggestions?: SuggestionRequest[]
notifications?: KilocodeNotification[]
status?: string
sessionID?: string
@@ -258,7 +242,6 @@ export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
id: props.sessionID,
permissions: props.permissions,
questions: props.questions,
suggestions: props.suggestions,
status: props.status,
})
const notifications = mockNotificationsValue(props.notifications)
@@ -12,10 +12,9 @@ import { StoryProviders, mockSessionValue } from "./StoryProviders"
import { ChatView } from "../components/chat/ChatView"
import { TaskHeader } from "../components/chat/TaskHeader"
import { QuestionDock } from "../components/chat/QuestionDock"
import { SuggestBar } from "../components/chat/SuggestBar"
import { SessionContext } from "../context/session"
import { ServerContext } from "../context/server"
import type { QuestionRequest, SuggestionRequest, TodoItem } from "../types/messages"
import type { QuestionRequest, TodoItem } from "../types/messages"
const SESSION_ID = "story-session-chat-001"
@@ -67,14 +66,6 @@ const multiQuestion: QuestionRequest = {
tool: { messageID: "asst-msg-001", callID: "call-question-002" },
}
const reviewSuggestion: SuggestionRequest = {
id: "s-review-001",
sessionID: SESSION_ID,
text: "Start a code review of uncommitted changes?",
actions: [{ label: "Start review", description: "Run a local review now", prompt: "/local-review-uncommitted" }],
tool: { messageID: "asst-msg-002", callID: "call-suggest-001" },
}
// ---------------------------------------------------------------------------
// Meta
// ---------------------------------------------------------------------------
@@ -181,17 +172,6 @@ export const QuestionDockManyOptions: Story = {
),
}
export const SuggestBarReview: Story = {
name: "SuggestBar — review suggestion",
render: () => (
<StoryProviders sessionID={SESSION_ID} suggestions={[reviewSuggestion]}>
<div style={{ width: "100%" }}>
<SuggestBar request={reviewSuggestion} />
</div>
</StoryProviders>
),
}
// ---------------------------------------------------------------------------
// TaskHeader with todos
// ---------------------------------------------------------------------------
@@ -19,6 +19,5 @@
@import "./notifications.css";
@import "./tool-overrides.css";
@import "./question-dock.css";
@import "./suggest-bar.css";
@import "./settings.css";
@import "./high-contrast.css";
@@ -1,49 +0,0 @@
/* ============================================
Suggest Bar
============================================ */
/* Strip the card border from tool-part-wrapper when it contains a suggest bar */
[data-component="tool-part-wrapper"]:has([data-component="suggest-bar"]) {
border: none !important;
border-radius: 0;
overflow: visible;
}
[data-component="suggest-bar"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0;
padding: 10px 12px;
background: color-mix(in srgb, var(--background-base) 88%, var(--vscode-textLink-foreground) 12%);
[data-slot="suggest-bar-copy"] {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
[data-slot="suggest-bar-icon"] {
display: inline-flex;
align-items: center;
color: var(--text-info, var(--vscode-textLink-foreground));
flex-shrink: 0;
}
[data-slot="suggest-bar-text"] {
min-width: 0;
color: var(--text-base);
font-size: 12px;
line-height: 1.4;
}
[data-slot="suggest-bar-actions"] {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
}
@@ -218,25 +218,6 @@ export interface QuestionRequest {
id: string
sessionID: string
questions: QuestionInfo[]
blocking?: boolean
tool?: {
messageID: string
callID: string
}
}
export interface SuggestionAction {
label: string
description?: string
prompt: string
}
export interface SuggestionRequest {
id: string
sessionID: string
text: string
actions: SuggestionAction[]
blocking?: boolean
tool?: {
messageID: string
callID: string
@@ -785,21 +766,6 @@ export interface QuestionErrorMessage {
requestID: string
}
export interface SuggestionRequestMessage {
type: "suggestionRequest"
suggestion: SuggestionRequest
}
export interface SuggestionResolvedMessage {
type: "suggestionResolved"
requestID: string
}
export interface SuggestionErrorMessage {
type: "suggestionError"
requestID: string
}
export interface BrowserSettings {
enabled: boolean
useSystemChrome: boolean
@@ -1530,9 +1496,6 @@ export type ExtensionMessage =
| QuestionRequestMessage
| QuestionResolvedMessage
| QuestionErrorMessage
| SuggestionRequestMessage
| SuggestionResolvedMessage
| SuggestionErrorMessage
| BrowserSettingsLoadedMessage
| ClaudeCompatSettingLoadedMessage
| ConfigLoadedMessage
@@ -1844,19 +1807,6 @@ export interface QuestionRejectRequest {
sessionID?: string
}
export interface SuggestionAcceptRequest {
type: "suggestionAccept"
requestID: string
sessionID: string
index: number
}
export interface SuggestionDismissRequest {
type: "suggestionDismiss"
requestID: string
sessionID: string
}
export interface DeleteSessionRequest {
type: "deleteSession"
sessionID: string
@@ -2493,8 +2443,6 @@ export type WebviewMessage =
| SetLanguageRequest
| QuestionReplyRequest
| QuestionRejectRequest
| SuggestionAcceptRequest
| SuggestionDismissRequest
| DeleteSessionRequest
| RenameSessionRequest
| RequestAutocompleteSettingsMessage
-2
View File
@@ -102,7 +102,6 @@ export namespace Agent {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
},
suggest: "deny", // kilocode_change
question: "deny",
plan_enter: "deny",
plan_exit: "deny",
@@ -131,7 +130,6 @@ export namespace Agent {
defaults,
Permission.fromConfig({
question: "allow",
suggest: "allow", // kilocode_change
plan_enter: "allow",
}),
user,
@@ -9,7 +9,6 @@ import type {
Command,
PermissionRequest,
QuestionRequest,
SuggestionRequest, // kilocode_change
SessionNetworkWait, // kilocode_change
LspStatus,
McpStatus,
@@ -28,7 +27,6 @@ import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit"
import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import { handleSuggestionEvent } from "@/kilocode/suggestion/tui/sync" // kilocode_change
import { Log } from "@/util/log"
import { useToast } from "@tui/ui/toast" // kilocode_change
import type { Path } from "@kilocode/sdk"
@@ -54,9 +52,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
[sessionID: string]: QuestionRequest[]
}
// kilocode_change start
suggestion: {
[sessionID: string]: SuggestionRequest[]
}
network: {
[sessionID: string]: SessionNetworkWait[]
}
@@ -103,7 +98,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
permission: {},
question: {},
// kilocode_change start
suggestion: {},
network: {},
// kilocode_change end
command: [],
@@ -149,9 +143,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
delete draft.session_diff[sessionID]
delete draft.session_status[sessionID]
delete draft.todo[sessionID]
delete draft.permission[sessionID]
delete draft.question[sessionID]
delete draft.suggestion[sessionID]
delete draft.network[sessionID]
}),
)
@@ -265,15 +256,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
// kilocode_change start
case "suggestion.accepted":
case "suggestion.dismissed":
case "suggestion.shown": {
handleSuggestionEvent(event, store, setStore)
break
}
// kilocode_change end
case "session.network.restored": {
const requests = store.network[event.properties.sessionID]
if (!requests) break
@@ -306,6 +288,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
// kilocode_change end
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
@@ -21,7 +21,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" // kilocode_change
import type { AssistantMessage, Part, Provider, ToolPart, UserMessage, TextPart, ReasoningPart } from "@kilocode/sdk/v2" // kilocode_change // kilocode_change
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool"
@@ -69,8 +69,6 @@ import { Filesystem } from "@/util/filesystem"
import { Global } from "@/global"
import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
import { Suggest } from "@/kilocode/suggestion/tui/render" // kilocode_change
import { SuggestPrompt } from "@/kilocode/suggestion/tui/prompt" // kilocode_change
import { NetworkPrompt } from "./network" // kilocode_change
import { DialogExportOptions } from "../../ui/dialog-export-options"
import * as Model from "../../util/model"
@@ -135,42 +133,18 @@ export function Session() {
return children().flatMap((x) => sync.data.question[x.id] ?? [])
})
// kilocode_change start
const suggestions = createMemo(() => {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.suggestion[x.id] ?? [])
})
const network = createMemo(() => {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.network[x.id] ?? [])
})
const blockingQuestions = createMemo(() => questions().filter((q) => q.blocking !== false))
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])
const visible = createMemo(
() =>
!session()?.parentID &&
permissions().length === 0 &&
blockingQuestions().length === 0 &&
blockingSuggestions().length === 0 &&
network().length === 0,
() => !session()?.parentID && permissions().length === 0 && questions().length === 0 && network().length === 0,
)
const networkVisible = createMemo(
() =>
permissions().length === 0 &&
blockingQuestions().length === 0 &&
blockingSuggestions().length === 0 &&
network().length > 0,
)
const disabled = createMemo(
() =>
permissions().length > 0 ||
blockingQuestions().length > 0 ||
blockingSuggestions().length > 0 ||
network().length > 0,
() => permissions().length === 0 && questions().length === 0 && network().length > 0,
)
const disabled = createMemo(() => permissions().length > 0 || questions().length > 0 || network().length > 0)
// kilocode_change end
const pending = createMemo(() => {
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
@@ -179,7 +153,6 @@ export function Session() {
const lastAssistant = createMemo(() => {
return messages().findLast((x) => x.role === "assistant")
})
// kilocode_change end
// kilocode_change start - ring terminal bell on task completion
createEffect(
@@ -214,7 +187,7 @@ export function Session() {
)
createEffect(
on(
() => [route.sessionID, suggestions().length + network().length] as const, // kilocode_change
() => [route.sessionID, network().length] as const,
([id, len], prev) => {
if (!prev || prev[0] !== id) return
if (len > prev[1] && bellEnabled()) bell()
@@ -1258,32 +1231,15 @@ export function Session() {
<PermissionPrompt request={permissions()[0]} />
</Show>
{/* kilocode_change start */}
<Show when={permissions().length === 0 && question()} keyed>
{(request) => (
<QuestionPrompt
request={request}
nonBlocking={request.blocking === false}
inputFocused={() => prompt?.focused ?? false}
/>
)}
</Show>
<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>
{/* kilocode_change start */}
<Show when={permissions().length === 0 && questions().length > 0}>
<QuestionPrompt request={questions()[0]} />
</Show>
{/* kilocode_change end */}
{/* kilocode_change end */}
<Show when={session()?.parentID}>
<SubagentFooter />
</Show>
{/* kilocode_change end */}
{/* kilocode_change start */}
<Show when={networkVisible()}>
<NetworkPrompt request={network()[0]} />
@@ -1701,11 +1657,6 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
<Match when={props.part.tool === "question"}>
<Question {...toolprops} />
</Match>
{/* kilocode_change start */}
<Match when={props.part.tool === "suggest"}>
<Suggest {...toolprops} InlineTool={InlineTool} BlockTool={BlockTool} />
</Match>
{/* kilocode_change end */}
<Match when={props.part.tool === "skill"}>
<Skill {...toolprops} />
</Match>
@@ -11,11 +11,7 @@ import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useDialog } from "../../ui/dialog"
// kilocode_change start
export function QuestionPrompt(props: {
request: QuestionRequest
nonBlocking?: boolean
inputFocused?: () => boolean
}) {
export function QuestionPrompt(props: { request: QuestionRequest }) {
// kilocode_change end
const sdk = useSDK()
const { theme } = useTheme()
@@ -132,10 +128,6 @@ export function QuestionPrompt(props: {
// Skip processing if a dialog (e.g., command palette) is open
if (dialog.stack.length > 0) return
// kilocode_change start - avoid intrusive key capture for non-blocking review suggestions
if (props.nonBlocking && props.inputFocused?.()) return
// kilocode_change end
// When editing custom answer textarea
if (store.editing && !confirm()) {
if (evt.name === "escape") {
@@ -1,2 +0,0 @@
// kilocode_change - new file
export { SuggestPrompt } from "../../../../../kilocode/suggestion/tui/prompt"
-6
View File
@@ -202,10 +202,4 @@ export namespace Command {
export async function list() {
return runPromise((svc) => svc.list())
}
// kilocode_change start
export async function get(name: string) {
return runPromise((svc) => svc.get(name))
}
// kilocode_change end
}
-1
View File
@@ -8,7 +8,6 @@ export namespace Identifier {
message: "msg",
permission: "per",
question: "que",
suggestion: "sug", // kilocode_change
user: "usr",
part: "prt",
pty: "pty",
@@ -5,7 +5,6 @@ import { Instance } from "@/project/instance"
import { Session } from "@/session"
import { SessionPrompt } from "@/session/prompt"
import { Question } from "@/question"
import { Suggestion } from "@/kilocode/suggestion" // kilocode_change
import { Permission } from "@/permission"
import { PermissionID } from "@/permission/schema"
import { SessionID } from "@/session/schema"
@@ -25,11 +24,6 @@ const PermissionData = z.object({
message: z.string().optional(),
})
const SuggestionData = z.object({
requestID: z.string(),
index: z.number().int().nonnegative(),
})
// kilocode_change start — lazy init to avoid circular dependency
// (Server → RemoteRoutes → RemoteSender → SessionPrompt at module load time)
let _remotePromptInput: ReturnType<typeof SessionPrompt.PromptInput.extend> | undefined
@@ -39,6 +33,7 @@ function getRemotePromptInput() {
}))
}
// kilocode_change end
function normalizeModel(model: string | undefined) {
if (!model) return undefined
return {
@@ -119,24 +114,11 @@ export namespace RemoteSender {
}
}
// Replay pending suggestions/questions/permissions so a newly-subscribed web client
// Replay pending questions/permissions so a newly-subscribed web client
// sees state that was asked before it connected — analogous to the Cloud
// Agent's `connected` event carrying pending question/permission fields.
async function replay(sessionId: string) {
const [suggestions, questions, permissions] = await Promise.all([
Suggestion.list(),
Question.list(),
Permission.list(),
])
for (const suggestion of suggestions) {
if (suggestion.sessionID !== sessionId) continue
options.conn.send({
type: "event",
sessionId,
event: "suggestion.shown",
data: suggestion,
})
}
const [questions, permissions] = await Promise.all([Question.list(), Permission.list()])
for (const q of questions) {
if (q.sessionID !== sessionId) continue
options.conn.send({
@@ -307,37 +289,6 @@ export namespace RemoteSender {
dispatchQuick(msg, dir, () => Question.reject(QuestionID.make(parsed.data.requestID)))
return
}
if (msg.command === "suggestion_accept") {
const parsed = SuggestionData.safeParse(msg.data)
if (!parsed.success) {
options.conn.send({
type: "response",
id: msg.id,
error: "invalid suggestion_accept data: " + parsed.error.message,
})
return
}
const dir = msg.sessionId ? directoryFor(msg.sessionId) : Promise.resolve(options.directory)
dispatchQuick(msg, dir, async () => {
const ok = await Suggestion.accept(parsed.data)
if (!ok) throw new Error("suggestion not found or invalid action index")
})
return
}
if (msg.command === "suggestion_dismiss") {
const parsed = z.object({ requestID: z.string() }).safeParse(msg.data)
if (!parsed.success) {
options.conn.send({
type: "response",
id: msg.id,
error: "invalid suggestion_dismiss data: " + parsed.error.message,
})
return
}
const dir = msg.sessionId ? directoryFor(msg.sessionId) : Promise.resolve(options.directory)
dispatchQuick(msg, dir, () => Suggestion.dismiss(parsed.data.requestID))
return
}
if (msg.command === "permission_respond") {
const parsed = PermissionData.safeParse(msg.data)
if (!parsed.success) {
@@ -231,7 +231,6 @@ export function patchAgents(
defaults,
Permission.fromConfig({
question: "allow",
suggest: "allow", // kilocode_change
plan_exit: "allow",
bash: readOnlyBash,
...kilo.mcpRules,
@@ -290,7 +289,6 @@ export function patchAgents(
defaults,
Permission.fromConfig({
question: "allow",
suggest: "allow", // kilocode_change
plan_enter: "allow",
}),
user,
@@ -314,7 +312,6 @@ export function patchAgents(
glob: "allow",
list: "allow",
question: "allow",
suggest: "allow", // kilocode_change
task: "allow",
todoread: "allow",
todowrite: "allow",
@@ -115,7 +115,6 @@ export async function generateHandover(input: {
export namespace PlanFollowup {
const log = Log.create({ service: "plan.followup" })
export const PLAN_PREFIX = "Implement the following plan:"
export const ANSWER_NEW_SESSION = "Start new session"
export const ANSWER_CONTINUE = "Continue here"
@@ -351,51 +351,27 @@ export namespace Review {
}
/**
* Get uncommitted changes (staged + unstaged + untracked)
* Get uncommitted changes (staged + unstaged)
* Implements SCOPE-01
*
* Uses: git diff HEAD for tracked changes, plus git ls-files for untracked files
* Uses: git diff HEAD to capture both staged and unstaged changes
*/
export async function getUncommittedChanges(): Promise<DiffResult> {
log.info("getting uncommitted changes")
// git diff HEAD shows all uncommitted changes (staged + unstaged) for tracked files
// git diff HEAD shows all uncommitted changes (staged + unstaged)
// Using -c core.quotepath=false to handle unicode filenames
const result = await $`git -c core.quotepath=false diff HEAD`.cwd(Instance.directory).quiet().nothrow()
let raw = result.exitCode === 0 ? result.stdout.toString() : ""
if (result.exitCode !== 0) {
log.warn("git diff failed", {
exitCode: result.exitCode,
stderr: result.stderr.toString(),
})
return { files: [], raw: "" }
}
// Also include untracked files — git diff HEAD misses brand-new files
const untracked = await $`git ls-files --others --exclude-standard -z`.cwd(Instance.directory).quiet().nothrow()
if (untracked.exitCode === 0) {
const paths = untracked.stdout.toString().split("\0").filter(Boolean)
// Process in batches to avoid spawning hundreds of git processes
const batch = 20
for (let i = 0; i < paths.length; i += batch) {
const chunk = paths.slice(i, i + batch)
const diffs = await Promise.all(
chunk.map((p) =>
// --no-index exits 1 when files differ, which is expected
$`git -c core.quotepath=false diff --no-index -- /dev/null ${p}`
.cwd(Instance.directory)
.quiet()
.nothrow()
.then((fd) => fd.stdout.toString()),
),
)
for (const out of diffs) {
if (out) raw += out
}
}
}
const raw = result.stdout.toString()
const parsed = parseDiff(raw)
log.info("parsed uncommitted changes", {
@@ -12,7 +12,6 @@ import { KilocodeRoutes } from "../../server/routes/kilocode"
import { PermissionKilocodeRoutes } from "../permission/routes"
import { RemoteRoutes } from "../../server/routes/remote"
import { NetworkRoutes } from "../../server/routes/network"
import { SuggestionRoutes } from "../suggestion/routes"
import { createKiloRoutes } from "@kilocode/kilo-gateway"
import { Auth } from "../../auth"
import { errors } from "../../server/error"
@@ -28,7 +27,6 @@ export function register(app: Hono): Hono {
return app
.route("/permission", PermissionKilocodeRoutes())
.route("/network", NetworkRoutes())
.route("/suggestion", SuggestionRoutes())
.route("/telemetry", TelemetryRoutes())
.route("/remote", RemoteRoutes())
.route("/commit-message", CommitMessageRoutes())
-13
View File
@@ -12,16 +12,3 @@ You are Kilo, a highly skilled software engineer with extensive knowledge in man
# Code
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
## Suggestions
- Use the `question` tool only when you need an actual answer from the user.
- If the `suggest` tool is available, use it ONLY to offer a local code review — never for other actions like committing, pushing, running tests, or any other next step.
- When you have completed implementation work and you are at least 90% confident the task is done, use `suggest` to offer a code review of uncommitted changes.
- Only suggest review when the user's request appears fully addressed. Do not suggest it after every edit or partial implementation turn.
- Do not repeat a review suggestion that was already dismissed in this conversation.
- Keep suggestion text concise, use at most 1-2 actions, and make each accepted action prompt self-contained.
- When suggesting a code review, choose the right command for the action prompt:
- `/local-review-uncommitted` — for reviewing uncommitted working-tree changes (staged, unstaged, and untracked files).
- `/local-review` — for reviewing all committed changes on the current branch vs its base branch.
- Prefer `/local-review-uncommitted` when the work you just did has not been committed yet.
@@ -1,194 +0,0 @@
import { Bus } from "../../bus"
import { BusEvent } from "../../bus/bus-event"
import { Identifier } from "../../id/id"
import { Instance } from "../../project/instance"
import { Log } from "../../util/log"
import z from "zod"
export namespace Suggestion {
const log = Log.create({ service: "suggestion" })
export const Action = z
.object({
label: z.string().describe("Button or option label (1-5 words)"),
description: z.string().optional().describe("Brief explanation of what this action does"),
prompt: z.string().describe("Synthetic user prompt to inject when this action is accepted"),
})
.meta({
ref: "SuggestionAction",
})
export type Action = z.infer<typeof Action>
export const Info = z
.object({
text: z.string().describe("Suggestion text shown to the user"),
actions: z.array(Action).min(1).max(2).describe("Available actions the user can take"),
})
.meta({
ref: "SuggestionInfo",
})
export type Info = z.infer<typeof Info>
export const Request = z
.object({
id: Identifier.schema("suggestion"),
sessionID: Identifier.schema("session"),
text: z.string().describe("Suggestion text shown to the user"),
actions: z.array(Action).min(1).max(2).describe("Available actions the user can take"),
blocking: z.boolean().optional().describe("Whether this suggestion blocks prompt input (default: true)"),
tool: z
.object({
messageID: z.string(),
callID: z.string(),
})
.optional(),
})
.meta({
ref: "SuggestionRequest",
})
export type Request = z.infer<typeof Request>
export const Accept = z.object({
index: z.number().int().nonnegative().describe("Zero-based action index to accept"),
})
export type Accept = z.infer<typeof Accept>
export const Event = {
Shown: BusEvent.define("suggestion.shown", Request),
Accepted: BusEvent.define(
"suggestion.accepted",
z.object({
sessionID: z.string(),
requestID: z.string(),
index: z.number().int().nonnegative(),
action: Action,
}),
),
Dismissed: BusEvent.define(
"suggestion.dismissed",
z.object({
sessionID: z.string(),
requestID: z.string(),
}),
),
}
const state = Instance.state(async () => {
const pending: Record<
string,
{
info: Request
resolve: (action: Action) => void
reject: (error: any) => void
}
> = {}
return {
pending,
}
})
export async function show(input: {
sessionID: string
text: string
actions: Action[]
blocking?: boolean
tool?: { messageID: string; callID: string }
}): Promise<Action> {
const s = await state()
const id = Identifier.ascending("suggestion")
log.info("shown", { id, actions: input.actions.length })
return new Promise<Action>((resolve, reject) => {
const info: Request = {
id,
sessionID: input.sessionID,
text: input.text,
actions: input.actions,
blocking: input.blocking,
tool: input.tool,
}
s.pending[id] = {
info,
resolve,
reject,
}
Bus.publish(Event.Shown, info)
})
}
export async function accept(input: { requestID: string; index: number }): Promise<boolean> {
const s = await state()
const existing = s.pending[input.requestID]
if (!existing) {
log.warn("accept for unknown request", { requestID: input.requestID })
return false
}
const action = existing.info.actions[input.index]
if (!action) {
log.warn("accept for invalid action index", { requestID: input.requestID, index: input.index })
delete s.pending[input.requestID]
existing.reject(new Error(`Invalid action index: ${input.index}`))
return false
}
delete s.pending[input.requestID]
log.info("accepted", { requestID: input.requestID, index: input.index, label: action.label })
Bus.publish(Event.Accepted, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
index: input.index,
action,
})
existing.resolve(action)
return true
}
export async function dismiss(requestID: string): Promise<void> {
const s = await state()
const existing = s.pending[requestID]
if (!existing) {
log.warn("dismiss for unknown request", { requestID })
return
}
delete s.pending[requestID]
log.info("dismissed", { requestID })
Bus.publish(Event.Dismissed, {
sessionID: existing.info.sessionID,
requestID: existing.info.id,
})
existing.reject(new DismissedError())
}
export class DismissedError extends Error {
constructor() {
super("The user dismissed this suggestion")
}
}
export async function dismissAll(sessionID: string): Promise<void> {
const s = await state()
for (const [id, entry] of Object.entries(s.pending)) {
if (entry.info.sessionID !== sessionID) continue
delete s.pending[id]
log.info("dismissed", { requestID: id })
Bus.publish(Event.Dismissed, {
sessionID: entry.info.sessionID,
requestID: entry.info.id,
})
entry.reject(new DismissedError())
}
}
export async function list() {
return state().then((state) => Object.values(state.pending).map((item) => item.info))
}
}
@@ -1,99 +0,0 @@
import { errors } from "../../server/error"
import { NotFoundError } from "../../storage/db"
import { lazy } from "../../util/lazy"
import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { Suggestion } from "./index"
export const SuggestionRoutes = lazy(() =>
new Hono()
.get(
"/",
describeRoute({
summary: "List pending suggestions",
description: "Get all pending suggestion requests across all sessions.",
operationId: "suggestion.list",
responses: {
200: {
description: "List of pending suggestions",
content: {
"application/json": {
schema: resolver(Suggestion.Request.array()),
},
},
},
},
}),
async (c) => {
const suggestions = await Suggestion.list()
return c.json(suggestions)
},
)
.post(
"/:requestID/accept",
describeRoute({
summary: "Accept suggestion request",
description: "Accept a suggestion request from the AI assistant.",
operationId: "suggestion.accept",
responses: {
200: {
description: "Suggestion accepted successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
requestID: z.string(),
}),
),
validator("json", Suggestion.Accept),
async (c) => {
const params = c.req.valid("param")
const json = c.req.valid("json")
const ok = await Suggestion.accept({
requestID: params.requestID,
index: json.index,
})
if (!ok) throw new NotFoundError({ message: `Suggestion not found: ${params.requestID}` })
return c.json(true)
},
)
.post(
"/:requestID/dismiss",
describeRoute({
summary: "Dismiss suggestion request",
description: "Dismiss a suggestion request from the AI assistant.",
operationId: "suggestion.dismiss",
responses: {
200: {
description: "Suggestion dismissed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
requestID: z.string(),
}),
),
async (c) => {
const params = c.req.valid("param")
await Suggestion.dismiss(params.requestID)
return c.json(true)
},
),
)
@@ -1,108 +0,0 @@
import { Command } from "../../command"
import { Flag } from "../../flag/flag"
import { Log } from "../../util/log"
import z from "zod"
import DESCRIPTION from "./tool.txt"
import { Tool } from "../../tool/tool"
import { Suggestion } from "./index"
const log = Log.create({ service: "tool.suggest" })
const Params = z.object({
suggest: z.string().describe("Short suggestion text shown to the user"),
actions: z.array(Suggestion.Action).min(1).max(2).describe("Available actions the user can take"),
})
type Meta = {
accepted?: Suggestion.Action
dismissed: boolean
truncated: boolean
}
/**
* If prompt starts with `/`, treat it as a slash-command reference.
* Resolve the command template and return its content so the LLM can
* act on it in the current turn without injecting a synthetic user
* message or trying to dispatch a command on the same session (which
* would deadlock).
*/
async function resolve(prompt: string): Promise<string> {
if (!prompt.startsWith("/")) return prompt
const name = prompt.slice(1).split(/\s/, 1)[0]
if (!name) return prompt
const args = prompt.slice(1 + name.length).trim()
const cmd = await Command.get(name)
if (!cmd) {
log.warn("unknown command in suggestion action", { name })
return prompt
}
try {
const template = await cmd.template
log.info("resolved command template", { name, length: template.length })
return args ? `${template}\n\n${args}` : template
} catch (err) {
log.warn("failed to resolve command template", { name, err })
return prompt
}
}
export const SuggestTool = Tool.define<typeof Params, Meta>("suggest", {
description: DESCRIPTION,
parameters: Params,
async execute(params, ctx) {
const promise = Suggestion.show({
sessionID: ctx.sessionID,
text: params.suggest,
actions: params.actions,
blocking: Flag.KILO_CLIENT !== "vscode",
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
})
const listener = () =>
Suggestion.list().then((items: Suggestion.Request[]) => {
const match = items.find((item: Suggestion.Request) => item.tool?.callID === ctx.callID)
if (match) return Suggestion.dismiss(match.id)
})
ctx.abort.addEventListener("abort", listener, { once: true })
const action = await promise
.catch((error) => {
if (error instanceof Suggestion.DismissedError) return undefined
throw error
})
.finally(() => {
ctx.abort.removeEventListener("abort", listener)
})
if (!action) {
const metadata: Meta = {
accepted: undefined,
dismissed: true,
truncated: false,
}
return {
title: "Suggestion dismissed",
output: "User dismissed the suggestion.",
metadata,
}
}
const resolved = await resolve(action.prompt)
const metadata: Meta = {
accepted: action,
dismissed: false,
truncated: false,
}
return {
title: `User accepted: ${action.label}`,
output: `User accepted the suggestion "${action.label}". Carry out the following request now:\n\n${resolved}`,
metadata,
}
},
})
@@ -1,17 +0,0 @@
Use this tool to suggest a local code review to the user after completing implementation work.
This tool is ONLY for suggesting code review. Do NOT use it to suggest running tests, committing, pushing, or any other action.
Guidelines:
- Only suggest review when you are at least 90% confident the user's request is fully addressed
- Do not suggest review after every edit or partial implementation turn
- Do not repeat a review suggestion that was already dismissed in this conversation
- Keep the suggestion text concise and actionable
- Provide 1-2 actions maximum
- Make each action prompt self-contained so it can be injected as a synthetic user message
- If you need a real answer from the user, use the `question` tool instead
Choosing the right review command for the action prompt:
- Use `/local-review-uncommitted` as the action prompt for uncommitted working-tree changes (staged, unstaged, and untracked files)
- Use `/local-review` as the action prompt for committed branch-level changes
- Prefer `/local-review-uncommitted` when the work you just did has not been committed yet
@@ -1,173 +0,0 @@
/** @jsxImportSource @opentui/solid */
import { useKeyboard } from "@opentui/solid"
import type { SuggestionRequest } from "@kilocode/sdk/v2"
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"
const dismiss = {
label: "Dismiss",
description: "Dismiss this suggestion and continue",
}
export function SuggestPrompt(props: {
request: SuggestionRequest
nonBlocking?: boolean
inputFocused?: () => boolean
}) {
const sdk = useSDK()
const { theme } = useTheme()
const keybind = useKeybind()
const dialog = useDialog()
const options = createMemo(() => [...props.request.actions, dismiss])
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)
})
}
function choose(index: number) {
if (index >= props.request.actions.length) {
reject()
return
}
accept(index)
}
useKeyboard((evt) => {
if (dialog.stack.length > 0) return
if (props.nonBlocking && props.inputFocused?.()) return
const total = options().length
const max = Math.min(total, 9)
const digit = Number(evt.name)
if (!Number.isNaN(digit) && digit >= 1 && digit <= max) {
evt.preventDefault()
const index = digit - 1
setSelected(index)
choose(index)
return
}
if (evt.name === "up" || evt.name === "k") {
evt.preventDefault()
setSelected((selected() - 1 + total) % total)
return
}
if (evt.name === "down" || evt.name === "j") {
evt.preventDefault()
setSelected((selected() + 1) % total)
return
}
if (evt.name === "return") {
evt.preventDefault()
choose(selected())
return
}
if (evt.name === "escape" || keybind.match("app_exit", evt)) {
evt.preventDefault()
reject()
}
})
const note = createMemo(() => (busy() ? "Waiting..." : undefined))
return (
<box
backgroundColor={theme.backgroundPanel}
border={["left"]}
borderColor={theme.secondary}
customBorderChars={SplitBorder.customBorderChars}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
<box paddingLeft={1}>
<text fg={theme.text}>{props.request.text}</text>
</box>
<box>
<For each={options()}>
{(opt, i) => {
const active = () => i() === selected()
const muted = () => i() === props.request.actions.length
return (
<box
onMouseOver={() => setSelected(i())}
onMouseDown={() => setSelected(i())}
onMouseUp={() => choose(i())}
>
<box flexDirection="row">
<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}>
<text fg={active() ? theme.secondary : muted() ? theme.textMuted : theme.text}>{opt.label}</text>
</box>
</box>
<box paddingLeft={3}>
<text fg={theme.textMuted}>{opt.description}</text>
</box>
</box>
)
}}
</For>
</box>
</box>
<box
flexDirection="row"
flexShrink={0}
gap={2}
paddingLeft={2}
paddingRight={3}
paddingBottom={1}
justifyContent="space-between"
>
<box flexDirection="row" gap={2}>
<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>
</box>
<text fg={theme.textMuted}>{note()}</text>
</box>
</box>
)
}
@@ -1,64 +0,0 @@
/** @jsxImportSource @opentui/solid */
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"
type InlineProps = {
icon: string
complete: unknown
pending: string
part: MessageToolPart
children: JSX.Element
}
type BlockProps = {
title: string
part?: MessageToolPart
children: JSX.Element
}
export function Suggest(props: {
input: {
suggest?: string
}
metadata: {
accepted?: {
label: string
}
dismissed?: boolean
}
part: MessageToolPart
InlineTool: (props: InlineProps) => JSX.Element
BlockTool: (props: BlockProps) => JSX.Element
}) {
const { theme } = useTheme()
const accepted = createMemo(() => props.metadata.accepted)
const dismissed = createMemo(() => props.metadata.dismissed === true)
if (accepted() || dismissed()) {
return props.BlockTool({
title: "# Suggestion",
part: props.part,
children: (
<box gap={1}>
<text fg={theme.textMuted}>{props.input.suggest}</text>
<Show when={accepted()}>
<text fg={theme.text}>Accepted: {accepted()?.label}</text>
</Show>
<Show when={dismissed()}>
<text fg={theme.text}>Dismissed</text>
</Show>
</box>
),
})
}
return props.InlineTool({
icon: "→",
pending: "Suggesting next step...",
complete: props.part.state.status === "completed",
part: props.part,
children: props.input.suggest ?? "Suggested next step",
})
}
@@ -1,58 +0,0 @@
import { Binary } from "@opencode-ai/util/binary"
import type { SuggestionRequest } from "@kilocode/sdk/v2"
type RemovedEvent = {
type: "suggestion.accepted" | "suggestion.dismissed"
properties: {
sessionID: string
requestID: string
}
}
type ShownEvent = {
type: "suggestion.shown"
properties: SuggestionRequest
}
type Event = RemovedEvent | ShownEvent
type Store = {
suggestion: {
[sessionID: string]: SuggestionRequest[]
}
}
type SetStore = {
(key: "suggestion", sessionID: string, value: SuggestionRequest[]): void
}
export function handleSuggestionEvent(event: Event, store: Store, setStore: SetStore) {
if (event.type !== "suggestion.shown") {
const info = event.properties
const requests = store.suggestion[info.sessionID]
if (!requests) return
const match = Binary.search(requests, info.requestID, (r) => r.id)
if (!match.found) return
setStore("suggestion", info.sessionID, requests.toSpliced(match.index, 1))
return
}
const request = event.properties
const requests = store.suggestion[request.sessionID]
if (!requests) {
setStore("suggestion", request.sessionID, [request])
return
}
const match = Binary.search(requests, request.id, (r) => r.id)
if (match.found) {
const next = [...requests]
next[match.index] = request
setStore("suggestion", request.sessionID, next)
return
}
setStore("suggestion", request.sessionID, [
...requests.slice(0, match.index),
request,
...requests.slice(match.index),
])
}
@@ -26,11 +26,6 @@ export namespace KiloToolRegistry {
return true
}
/** Suggest tool is only registered for cli and vscode clients */
export function suggest(tool: Tool.Def): Tool.Def[] {
return ["cli", "vscode"].includes(Flag.KILO_CLIENT) ? [tool] : []
}
/** Kilo-specific tools to append to the builtin list */
export function extra(
tools: { codebase: Tool.Def; recall: Tool.Def },
-5
View File
@@ -37,7 +37,6 @@ export namespace Question {
id: QuestionID.zod,
sessionID: SessionID.zod,
questions: z.array(Info).describe("Questions to ask"),
blocking: z.boolean().optional().describe("Whether this question blocks prompt input (default: true)"), // kilocode_change
tool: z
.object({
messageID: MessageID.zod,
@@ -98,7 +97,6 @@ export namespace Question {
readonly ask: (input: {
sessionID: SessionID
questions: Info[]
blocking?: boolean // kilocode_change
tool?: { messageID: MessageID; callID: string }
}) => Effect.Effect<Answer[], RejectedError>
readonly reply: (input: { requestID: QuestionID; answers: Answer[] }) => Effect.Effect<void>
@@ -134,7 +132,6 @@ export namespace Question {
const ask = Effect.fn("Question.ask")(function* (input: {
sessionID: SessionID
questions: Info[]
blocking?: boolean // kilocode_change
tool?: { messageID: MessageID; callID: string }
}) {
const pending = (yield* InstanceState.get(state)).pending
@@ -146,7 +143,6 @@ export namespace Question {
id,
sessionID: input.sessionID,
questions: input.questions,
blocking: input.blocking, // kilocode_change
tool: input.tool,
}
pending.set(id, { info, deferred })
@@ -209,7 +205,6 @@ export namespace Question {
export async function ask(input: {
sessionID: SessionID
questions: Info[]
blocking?: boolean // kilocode_change
tool?: { messageID: MessageID; callID: string }
}): Promise<Answer[]> {
return runPromise((s) => s.ask(input))
@@ -1,2 +0,0 @@
// kilocode_change - new file
export { SuggestionRoutes } from "../../kilocode/suggestion/routes"
+1 -11
View File
@@ -19,7 +19,6 @@ import { SessionSummary } from "./summary"
import type { Provider } from "@/provider/provider"
import { Question } from "@/question"
import { KiloSessionProcessor } from "@/kilocode/session/processor" // kilocode_change
import { Suggestion } from "@/kilocode/suggestion" // kilocode_change
import { errorMessage } from "@/util/error"
import { isRecord } from "@/util/record"
@@ -209,11 +208,7 @@ export namespace SessionProcessor {
},
})
// kilocode_change start
if (
error instanceof Permission.RejectedError ||
error instanceof Question.RejectedError ||
error instanceof Suggestion.DismissedError
) {
if (error instanceof Permission.RejectedError || error instanceof Question.RejectedError) {
// kilocode_change end
ctx.blocked = ctx.shouldBreak
}
@@ -362,11 +357,6 @@ export namespace SessionProcessor {
case "tool-result": {
yield* completeToolCall(value.toolCallId, value.output)
// kilocode_change start
if (value.output.metadata?.dismissed === true) {
ctx.blocked = ctx.shouldBreak
}
// kilocode_change end
return
}
-6
View File
@@ -3,7 +3,6 @@ import os from "os"
import fs from "fs/promises"
import { KiloSessionPrompt } from "@/kilocode/session/prompt" // kilocode_change
import { KiloSession } from "@/kilocode/session" // kilocode_change
import { Suggestion } from "@/kilocode/suggestion" // kilocode_change
import z from "zod"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
@@ -1288,11 +1287,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
if (input.noReply === true) return message
// kilocode_change start — dismiss pending suggestions and cancel the session
// before starting a new loop to avoid the runner ignoring the new work
yield* Effect.promise(() => Suggestion.dismissAll(input.sessionID))
yield* state.cancel(input.sessionID)
// kilocode_change end
return yield* loop({ sessionID: input.sessionID })
},
)
@@ -1,2 +0,0 @@
// kilocode_change - new file
export { Suggestion } from "../kilocode/suggestion/index"
-3
View File
@@ -1,6 +1,5 @@
import { PlanExitTool } from "./plan"
import { QuestionTool } from "./question"
import { SuggestTool } from "../kilocode/suggestion/tool" // kilocode_change
import { BashTool } from "./bash"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
@@ -162,7 +161,6 @@ export namespace ToolRegistry {
question: Tool.init(question),
lsp: Tool.init(LspTool),
plan: Tool.init(PlanExitTool),
suggest: Tool.init(SuggestTool), // kilocode_change
})
const kilo = yield* KiloToolRegistry.build() // kilocode_change
@@ -187,7 +185,6 @@ export namespace ToolRegistry {
tool.patch,
...(Flag.KILO_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []), // kilocode_change
...(KiloToolRegistry.plan() ? [tool.plan] : []), // kilocode_change
...KiloToolRegistry.suggest(tool.suggest), // kilocode_change
...KiloToolRegistry.extra(kilo, cfg), // kilocode_change
],
task: tool.task,
-2
View File
@@ -1,2 +0,0 @@
// kilocode_change - new file
export { SuggestTool } from "../kilocode/suggestion/tool"
@@ -6,7 +6,6 @@ import type { RemoteProtocol } from "../../../src/kilo-sessions/remote-protocol"
import { SessionPrompt } from "../../../src/session/prompt"
import { Question } from "../../../src/question"
import { Permission } from "../../../src/permission"
import { Suggestion } from "../../../src/kilocode/suggestion" // kilocode_change
function fakeConn() {
const sent: any[] = []
@@ -491,51 +490,6 @@ describe("RemoteSender", () => {
expect(sent[0].error).toContain("boom")
})
test("suggestion_accept sends response after work completes", async () => {
const { conn, sent } = fakeConn()
const accept = spyOn(Suggestion, "accept").mockResolvedValue(true)
const sender = RemoteSender.create({
conn,
directory: "/tmp/test",
log: nolog,
subscribe: fakeBus().subscribe,
provide: async <R>(input: { directory: string; init?: () => Promise<unknown>; fn: () => R }) => input.fn(),
})
sender.handle({
type: "command",
id: "req_suggestion_accept",
command: "suggestion_accept",
data: { requestID: "sug_1", index: 1 },
})
await new Promise((r) => setTimeout(r, 10))
expect(accept).toHaveBeenCalledWith({ requestID: "sug_1", index: 1 })
expect(sent).toContainEqual({ type: "response", id: "req_suggestion_accept", result: {} })
})
test("suggestion_dismiss with invalid data sends error response", () => {
const { conn, sent } = fakeConn()
const sender = RemoteSender.create({
conn,
directory: "/tmp/test",
log: nolog,
subscribe: fakeBus().subscribe,
provide: async () => ({}) as any,
})
sender.handle({
type: "command",
id: "req_suggestion_dismiss_bad",
command: "suggestion_dismiss",
data: { nope: true },
})
expect(sent).toHaveLength(1)
expect(sent[0].error).toContain("invalid suggestion_dismiss data")
})
test("question_reject sends response after work completes", async () => {
const { conn, sent } = fakeConn()
let provideCalled = false
@@ -859,7 +813,6 @@ describe("RemoteSender", () => {
const { conn, sent } = fakeConn()
const bus = fakeBus()
spyOn(Suggestion, "list").mockResolvedValue([])
spyOn(Question, "list").mockResolvedValue([
{ id: "question_1", sessionID: "ses_target", questions: [{ type: "text", text: "Continue?" }] } as any,
{ id: "question_2", sessionID: "ses_other", questions: [{ type: "text", text: "Unrelated?" }] } as any,
@@ -891,7 +844,6 @@ describe("RemoteSender", () => {
const { conn, sent } = fakeConn()
const bus = fakeBus()
spyOn(Suggestion, "list").mockResolvedValue([])
spyOn(Question, "list").mockResolvedValue([])
spyOn(Permission, "list").mockResolvedValue([
{
@@ -944,9 +896,6 @@ describe("RemoteSender", () => {
const { conn, sent } = fakeConn()
const bus = fakeBus()
spyOn(Suggestion, "list").mockResolvedValue([
{ id: "sug_1", sessionID: "ses_other", text: "Review?", actions: [] } as any,
])
spyOn(Question, "list").mockResolvedValue([{ id: "question_1", sessionID: "ses_other", questions: [] } as any])
spyOn(Permission, "list").mockResolvedValue([
{
@@ -974,53 +923,6 @@ describe("RemoteSender", () => {
expect(events).toHaveLength(0)
})
test("subscribe replays pending suggestion for the subscribed session", async () => {
const { conn, sent } = fakeConn()
const bus = fakeBus()
spyOn(Suggestion, "list").mockResolvedValue([
{
id: "sug_1",
sessionID: "ses_target",
text: "Review?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
} as any,
{
id: "sug_2",
sessionID: "ses_other",
text: "Ignore",
actions: [{ label: "Skip", prompt: "skip" }],
} as any,
])
spyOn(Question, "list").mockResolvedValue([])
spyOn(Permission, "list").mockResolvedValue([])
const sender = RemoteSender.create({
conn,
directory: "/tmp/test",
log: nolog,
subscribe: bus.subscribe,
provide: async (input: any) => input.fn(),
})
sender.handle({ type: "subscribe", sessionId: "ses_target" })
await new Promise((r) => setTimeout(r, 10))
const suggestionEvents = sent.filter((m: any) => m.event === "suggestion.shown")
expect(suggestionEvents).toHaveLength(1)
expect(suggestionEvents[0]).toEqual({
type: "event",
sessionId: "ses_target",
event: "suggestion.shown",
data: {
id: "sug_1",
sessionID: "ses_target",
text: "Review?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
},
})
})
test("system message is handled without error", () => {
const { conn, sent } = fakeConn()
const sender = RemoteSender.create({
@@ -1,76 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Instance } from "../../../src/project/instance"
import { Suggestion } from "../../../src/kilocode/suggestion"
import { tmpdir } from "../../fixture/fixture"
describe("suggestion", () => {
test("show adds pending request with blocking flag", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const pending = Suggestion.show({
sessionID: "ses_test",
text: "Run review?",
blocking: false,
actions: [{ label: "Start", description: "Run it", prompt: "/local-review-uncommitted" }],
})
const list = await Suggestion.list()
expect(list).toHaveLength(1)
expect(list[0]?.blocking).toBe(false)
expect(list[0]?.text).toBe("Run review?")
await Suggestion.dismiss(list[0]!.id)
await expect(pending).rejects.toBeInstanceOf(Suggestion.DismissedError)
},
})
})
test("accept resolves selected action and removes pending request", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ask = Suggestion.show({
sessionID: "ses_test",
text: "Next step?",
actions: [
{ label: "Review", description: "Start review", prompt: "/local-review-uncommitted" },
{ label: "Test", description: "Run tests", prompt: "Run the relevant tests now." },
],
})
const list = await Suggestion.list()
await Suggestion.accept({ requestID: list[0]!.id, index: 1 })
await expect(ask).resolves.toEqual({
label: "Test",
description: "Run tests",
prompt: "Run the relevant tests now.",
})
await expect(Suggestion.list()).resolves.toEqual([])
},
})
})
test("dismiss rejects pending request and removes it", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ask = Suggestion.show({
sessionID: "ses_test",
text: "Review changes?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
})
const list = await Suggestion.list()
await Suggestion.dismiss(list[0]!.id)
await expect(ask).rejects.toBeInstanceOf(Suggestion.DismissedError)
await expect(Suggestion.list()).resolves.toEqual([])
},
})
})
})
@@ -1,162 +0,0 @@
import { afterEach, beforeEach, describe, expect, test, spyOn } from "bun:test"
import { Command } from "../../../src/command"
import { Suggestion } from "../../../src/kilocode/suggestion"
import { SuggestTool } from "../../../src/kilocode/suggestion/tool"
const ctx = {
sessionID: "ses_test",
messageID: "msg_assistant",
callID: "call_suggest",
agent: "code",
abort: AbortSignal.any([]),
messages: [
{
info: {
id: "msg_user",
role: "user",
sessionID: "ses_test",
time: { created: 1 },
agent: "code",
model: { providerID: "openai", modelID: "gpt-4" },
},
parts: [],
},
],
metadata: () => {},
ask: async () => {},
}
describe("tool.suggest", () => {
let show: ReturnType<typeof spyOn>
let cmdGet: ReturnType<typeof spyOn>
beforeEach(() => {
show = spyOn(Suggestion, "show")
cmdGet = spyOn(Command, "get")
})
afterEach(() => {
show.mockRestore()
cmdGet.mockRestore()
})
test("returns dismissal result when suggestion is dismissed", async () => {
const tool = await SuggestTool.init()
show.mockRejectedValueOnce(new Suggestion.DismissedError())
const result = await tool.execute(
{
suggest: "Run review?",
actions: [{ label: "Start", prompt: "/local-review-uncommitted" }],
},
ctx as any,
)
expect(result.title).toBe("Suggestion dismissed")
expect(result.output).toBe("User dismissed the suggestion.")
expect(result.metadata.dismissed).toBe(true)
})
test("resolves command template for slash-command action prompt", async () => {
const tool = await SuggestTool.init()
show.mockResolvedValueOnce({
label: "Start review",
description: "Run a local review now",
prompt: "/local-review-uncommitted",
})
cmdGet.mockResolvedValueOnce({
name: "local-review-uncommitted",
description: "local review (uncommitted changes)",
template: Promise.resolve("Review these uncommitted changes:\n\n## Files Changed\n..."),
hints: [],
})
const result = await tool.execute(
{
suggest: "Run review?",
actions: [{ label: "Start review", prompt: "/local-review-uncommitted" }],
},
ctx as any,
)
expect(result.title).toBe("User accepted: Start review")
expect(result.output).toContain("Review these uncommitted changes:")
expect(result.output).toContain("Carry out the following request now")
expect(result.metadata.dismissed).toBe(false)
expect(result.metadata.accepted).toEqual({
label: "Start review",
description: "Run a local review now",
prompt: "/local-review-uncommitted",
})
expect(cmdGet).toHaveBeenCalledWith("local-review-uncommitted")
})
test("returns plain-text prompt directly for non-command actions", async () => {
const tool = await SuggestTool.init()
show.mockResolvedValueOnce({
label: "Run tests",
prompt: "Run the test suite and fix any failures",
})
const result = await tool.execute(
{
suggest: "Tests might need running",
actions: [{ label: "Run tests", prompt: "Run the test suite and fix any failures" }],
},
ctx as any,
)
expect(result.title).toBe("User accepted: Run tests")
expect(result.output).toContain("Run the test suite and fix any failures")
expect(result.output).toContain("Carry out the following request now")
expect(result.metadata.dismissed).toBe(false)
expect(cmdGet).not.toHaveBeenCalled()
})
test("falls back to raw prompt when command is not found", async () => {
const tool = await SuggestTool.init()
show.mockResolvedValueOnce({
label: "Unknown cmd",
prompt: "/nonexistent-command",
})
cmdGet.mockResolvedValueOnce(undefined)
const result = await tool.execute(
{
suggest: "Try this?",
actions: [{ label: "Unknown cmd", prompt: "/nonexistent-command" }],
},
ctx as any,
)
expect(result.title).toBe("User accepted: Unknown cmd")
expect(result.output).toContain("/nonexistent-command")
expect(result.metadata.dismissed).toBe(false)
})
test("falls back to raw prompt when template resolution fails", async () => {
const tool = await SuggestTool.init()
show.mockResolvedValueOnce({
label: "Start review",
prompt: "/local-review-uncommitted",
})
cmdGet.mockResolvedValueOnce({
name: "local-review-uncommitted",
description: "local review (uncommitted changes)",
template: Promise.reject(new Error("git not found")),
hints: [],
})
const result = await tool.execute(
{
suggest: "Run review?",
actions: [{ label: "Start review", prompt: "/local-review-uncommitted" }],
},
ctx as any,
)
expect(result.title).toBe("User accepted: Start review")
expect(result.output).toContain("/local-review-uncommitted")
expect(result.metadata.dismissed).toBe(false)
})
})
@@ -72,34 +72,6 @@ test("ask - adds to pending list", async () => {
})
})
// kilocode_change start - review follow-up uses non-blocking question prompts
test("ask - preserves blocking flag", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const askPromise = Question.ask({
sessionID: SessionID.make("ses_test"),
blocking: false,
questions: [
{
question: "Proceed with review suggestion?",
header: "Code review",
options: [{ label: "Start", description: "Run review" }],
},
],
})
const pending = await Question.list()
expect(pending[0]?.blocking).toBe(false)
await Question.reject(pending[0].id)
await expect(askPromise).rejects.toBeInstanceOf(Question.RejectedError)
},
})
})
// kilocode_change end
// reply tests
test("reply - resolves the pending ask with answers", async () => {
@@ -1,2 +0,0 @@
// kilocode_change - new file
// Moved to test/kilocode/suggestion/suggestion.test.ts.
@@ -32,37 +32,6 @@ describe("tool.registry", () => {
})
// kilocode_change end
// kilocode_change start
test("suggest is registered for cli and vscode only", async () => {
const original = process.env["KILO_CLIENT"]
const originalQuestion = process.env["KILO_ENABLE_QUESTION_TOOL"]
const originalConfig = process.env["KILO_CONFIG_DIR"]
try {
for (const client of ["cli", "vscode", "desktop", "app"]) {
process.env["KILO_CLIENT"] = client
process.env["KILO_ENABLE_QUESTION_TOOL"] = client === "vscode" ? "true" : "false"
await using tmp = await tmpdir({ git: true })
process.env["KILO_CONFIG_DIR"] = tmp.path
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
if (client === "cli" || client === "vscode") expect(ids).toContain("suggest")
else expect(ids).not.toContain("suggest")
},
})
}
} finally {
if (original === undefined) delete process.env["KILO_CLIENT"]
else process.env["KILO_CLIENT"] = original
if (originalQuestion === undefined) delete process.env["KILO_ENABLE_QUESTION_TOOL"]
else process.env["KILO_ENABLE_QUESTION_TOOL"] = originalQuestion
if (originalConfig === undefined) delete process.env["KILO_CONFIG_DIR"]
else process.env["KILO_CONFIG_DIR"] = originalConfig
}
})
// kilocode_change end
test("loads tools from .opencode/tool (singular)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
@@ -1,2 +0,0 @@
// kilocode_change - new file
// Moved to test/kilocode/suggestion/tool.test.ts.
-113
View File
@@ -203,11 +203,6 @@ import type {
SessionUpdateResponses,
SessionViewedResponses,
SubtaskPartInput,
SuggestionAcceptErrors,
SuggestionAcceptResponses,
SuggestionDismissErrors,
SuggestionDismissResponses,
SuggestionListResponses,
TelemetryCaptureErrors,
TelemetryCaptureResponses,
TextPartInput,
@@ -4521,109 +4516,6 @@ export class Network extends HeyApiClient {
}
}
export class Suggestion extends HeyApiClient {
/**
* List pending suggestions
*
* Get all pending suggestion requests across all sessions.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<SuggestionListResponses, unknown, ThrowOnError>({
url: "/suggestion",
...options,
...params,
})
}
/**
* Accept suggestion request
*
* Accept a suggestion request from the AI assistant.
*/
public accept<ThrowOnError extends boolean = false>(
parameters: {
requestID: string
directory?: string
workspace?: string
index?: number
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "requestID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "index" },
],
},
],
)
return (options?.client ?? this.client).post<SuggestionAcceptResponses, SuggestionAcceptErrors, ThrowOnError>({
url: "/suggestion/{requestID}/accept",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Dismiss suggestion request
*
* Dismiss a suggestion request from the AI assistant.
*/
public dismiss<ThrowOnError extends boolean = false>(
parameters: {
requestID: string
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "requestID" },
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).post<SuggestionDismissResponses, SuggestionDismissErrors, ThrowOnError>({
url: "/suggestion/{requestID}/dismiss",
...options,
...params,
})
}
}
export class Telemetry extends HeyApiClient {
/**
* Capture telemetry event
@@ -5829,11 +5721,6 @@ export class KiloClient extends HeyApiClient {
return (this._network ??= new Network({ client: this.client }))
}
private _suggestion?: Suggestion
get suggestion(): Suggestion {
return (this._suggestion ??= new Suggestion({ client: this.client }))
}
private _telemetry?: Telemetry
get telemetry(): Telemetry {
return (this._telemetry ??= new Telemetry({ client: this.client }))
-158
View File
@@ -418,10 +418,6 @@ export type QuestionRequest = {
* Questions to ask
*/
questions: Array<QuestionInfo>
/**
* Whether this question blocks prompt input (default: true)
*/
blocking?: boolean
tool?: {
messageID: string
callID: string
@@ -509,65 +505,6 @@ export type EventSessionIdle = {
}
}
export type SuggestionAction = {
/**
* Button or option label (1-5 words)
*/
label: string
/**
* Brief explanation of what this action does
*/
description?: string
/**
* Synthetic user prompt to inject when this action is accepted
*/
prompt: string
}
export type SuggestionRequest = {
id: string
sessionID: string
/**
* Suggestion text shown to the user
*/
text: string
/**
* Available actions the user can take
*/
actions: Array<SuggestionAction>
/**
* Whether this suggestion blocks prompt input (default: true)
*/
blocking?: boolean
tool?: {
messageID: string
callID: string
}
}
export type EventSuggestionShown = {
type: "suggestion.shown"
properties: SuggestionRequest
}
export type EventSuggestionAccepted = {
type: "suggestion.accepted"
properties: {
sessionID: string
requestID: string
index: number
action: SuggestionAction
}
}
export type EventSuggestionDismissed = {
type: "suggestion.dismissed"
properties: {
sessionID: string
requestID: string
}
}
export type EventSessionCompacted = {
type: "session.compacted"
properties: {
@@ -1147,9 +1084,6 @@ export type Event =
| EventTodoUpdated
| EventSessionStatus
| EventSessionIdle
| EventSuggestionShown
| EventSuggestionAccepted
| EventSuggestionDismissed
| EventSessionCompacted
| EventKiloSessionsRemoteStatusChanged
| EventWorkspaceReady
@@ -5757,98 +5691,6 @@ export type NetworkRejectResponses = {
export type NetworkRejectResponse = NetworkRejectResponses[keyof NetworkRejectResponses]
export type SuggestionListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/suggestion"
}
export type SuggestionListResponses = {
/**
* List of pending suggestions
*/
200: Array<SuggestionRequest>
}
export type SuggestionListResponse = SuggestionListResponses[keyof SuggestionListResponses]
export type SuggestionAcceptData = {
body?: {
/**
* Zero-based action index to accept
*/
index: number
}
path: {
requestID: string
}
query?: {
directory?: string
workspace?: string
}
url: "/suggestion/{requestID}/accept"
}
export type SuggestionAcceptErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SuggestionAcceptError = SuggestionAcceptErrors[keyof SuggestionAcceptErrors]
export type SuggestionAcceptResponses = {
/**
* Suggestion accepted successfully
*/
200: boolean
}
export type SuggestionAcceptResponse = SuggestionAcceptResponses[keyof SuggestionAcceptResponses]
export type SuggestionDismissData = {
body?: never
path: {
requestID: string
}
query?: {
directory?: string
workspace?: string
}
url: "/suggestion/{requestID}/dismiss"
}
export type SuggestionDismissErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SuggestionDismissError = SuggestionDismissErrors[keyof SuggestionDismissErrors]
export type SuggestionDismissResponses = {
/**
* Suggestion dismissed successfully
*/
200: boolean
}
export type SuggestionDismissResponse = SuggestionDismissResponses[keyof SuggestionDismissResponses]
export type TelemetryCaptureData = {
body?: {
/**
-213
View File
@@ -5247,206 +5247,6 @@
]
}
},
"/suggestion": {
"get": {
"operationId": "suggestion.list",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
}
],
"summary": "List pending suggestions",
"description": "Get all pending suggestion requests across all sessions.",
"responses": {
"200": {
"description": "List of pending suggestions",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SuggestionRequest"
}
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.suggestion.list({\n ...\n})"
}
]
}
},
"/suggestion/{requestID}/accept": {
"post": {
"operationId": "suggestion.accept",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "requestID",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Accept suggestion request",
"description": "Accept a suggestion request from the AI assistant.",
"responses": {
"200": {
"description": "Suggestion accepted successfully",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"index": {
"description": "Zero-based action index to accept",
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": ["index"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.suggestion.accept({\n ...\n})"
}
]
}
},
"/suggestion/{requestID}/dismiss": {
"post": {
"operationId": "suggestion.dismiss",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "requestID",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Dismiss suggestion request",
"description": "Dismiss a suggestion request from the AI assistant.",
"responses": {
"200": {
"description": "Suggestion dismissed successfully",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.suggestion.dismiss({\n ...\n})"
}
]
}
},
"/provider": {
"get": {
"operationId": "provider.list",
@@ -11178,7 +10978,6 @@
},
"required": ["name", "data"]
},
"Event.session.error": {
"type": "object",
"properties": {
@@ -13284,18 +13083,6 @@
{
"$ref": "#/components/schemas/Event.lsp.updated"
},
{
"$ref": "#/components/schemas/Event.file.edited"
},
{
"$ref": "#/components/schemas/Event.suggestion.shown"
},
{
"$ref": "#/components/schemas/Event.suggestion.accepted"
},
{
"$ref": "#/components/schemas/Event.suggestion.dismissed"
},
{
"$ref": "#/components/schemas/Event.tui.prompt.append"
},