mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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:
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user