From 8094ba6ab3c5a729e0ca32effd0cb5786afd0946 Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Thu, 4 Jun 2026 16:30:01 -0600 Subject: [PATCH 1/5] feat(cli): suggest skill slash commands --- packages/opencode/src/cli/cmd/run/footer.prompt.tsx | 5 +++-- .../src/cli/cmd/tui/component/prompt/autocomplete.tsx | 5 ++--- .../opencode/src/kilocode/cli/cmd/command-display.ts | 10 ++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/src/kilocode/cli/cmd/command-display.ts diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx index 8cd4fbfcf58..153c67449b1 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx @@ -11,6 +11,7 @@ import { useKeyboard } from "@opentui/solid" import fuzzysort from "fuzzysort" import path from "path" import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js" +import { slashDisplay } from "@/kilocode/cli/cmd/command-display" // kilocode_change import * as Locale from "@/util/locale" import { createPromptHistory, @@ -375,13 +376,13 @@ export function createPromptState(input: PromptInput): PromptState { const hidden = new Set(builtins.map((item) => item.name)) return [ ...(input.commands() ?? []) - .filter((item) => item.source !== "skill" && !hidden.has(item.name)) + .filter((item) => !hidden.has(item.name)) // kilocode_change - suggest skills as slash commands .map( (item) => ({ kind: "slash", name: item.name, - display: `/${item.name}${item.source === "mcp" ? ":mcp" : ""}`, + display: slashDisplay(item), // kilocode_change description: item.description, }) satisfies SlashOption, ), diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 12e628e19de..1aa5fee041a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -14,6 +14,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useCommandPalette } from "../../context/command-palette" import { useTerminalDimensions } from "@opentui/solid" +import { slashDisplay } from "@/kilocode/cli/cmd/command-display" // kilocode_change import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" @@ -405,10 +406,8 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = [...command.slashes()] for (const serverCommand of sync.data.command) { - if (serverCommand.source === "skill") continue - const label = serverCommand.source === "mcp" ? ":mcp" : "" results.push({ - display: "/" + serverCommand.name + label, + display: slashDisplay(serverCommand), // kilocode_change description: serverCommand.description, onSelect: () => { const newText = "/" + serverCommand.name + " " diff --git a/packages/opencode/src/kilocode/cli/cmd/command-display.ts b/packages/opencode/src/kilocode/cli/cmd/command-display.ts new file mode 100644 index 00000000000..5a498fda61f --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/command-display.ts @@ -0,0 +1,10 @@ +type Command = { + name: string + source?: "command" | "mcp" | "skill" +} + +export function slashDisplay(cmd: Command) { + if (cmd.source === "skill") return `/${cmd.name}:skill` + if (cmd.source === "mcp") return `/${cmd.name}:mcp` + return `/${cmd.name}` +} From 881a451f8ac198c9d199616c1eef20e94ff25b57 Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Thu, 4 Jun 2026 17:01:39 -0600 Subject: [PATCH 2/5] Changeset --- .changeset/floppy-ads-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/floppy-ads-rule.md diff --git a/.changeset/floppy-ads-rule.md b/.changeset/floppy-ads-rule.md new file mode 100644 index 00000000000..924f8032c92 --- /dev/null +++ b/.changeset/floppy-ads-rule.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Display skills in CLI slash command autocomplete options From 2099feba9bbdcbc5103ffeccf0faeea27719975c Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Thu, 4 Jun 2026 22:38:19 -0600 Subject: [PATCH 3/5] fix(cli): disambiguate skill slash commands --- .changeset/fuzzy-skill-conflicts.md | 5 ++ .../src/cli/cmd/run/footer.prompt.tsx | 6 +- .../cmd/tui/component/prompt/autocomplete.tsx | 7 ++- .../cli/cmd/tui/component/prompt/index.tsx | 3 +- packages/opencode/src/command/index.ts | 62 +++++++++++++++---- .../src/kilocode/cli/cmd/command-display.ts | 4 ++ .../skill-command-autocomplete.test.ts | 57 +++++++++++++++++ 7 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 .changeset/fuzzy-skill-conflicts.md create mode 100644 packages/opencode/test/kilocode/skill-command-autocomplete.test.ts diff --git a/.changeset/fuzzy-skill-conflicts.md b/.changeset/fuzzy-skill-conflicts.md new file mode 100644 index 00000000000..9cf1abec929 --- /dev/null +++ b/.changeset/fuzzy-skill-conflicts.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix slash autocomplete and invocation for skills that share a name with commands. diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx index 153c67449b1..ad0e1508e6e 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx @@ -11,7 +11,7 @@ import { useKeyboard } from "@opentui/solid" import fuzzysort from "fuzzysort" import path from "path" import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js" -import { slashDisplay } from "@/kilocode/cli/cmd/command-display" // kilocode_change +import { slashDisplay, slashMatches } from "@/kilocode/cli/cmd/command-display" // kilocode_change import * as Locale from "@/util/locale" import { createPromptHistory, @@ -170,7 +170,7 @@ function parseSlashCommand(text: string, commands: RunCommand[] | undefined) { return { type: "pending" as const } } - if (!commands.some((item) => item.name === head.name)) { + if (!commands.some((item) => slashMatches(item, head.name))) { // kilocode_change return { type: "none" as const } } @@ -764,7 +764,7 @@ export function createPromptState(input: PromptInput): PromptState { } if (next.kind === "slash") { - const text = `/${next.name} ` + const text = `${next.display} ` // kilocode_change const cursor = area.cursorOffset area.cursorOffset = 0 diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 1aa5fee041a..5908789bba3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -406,17 +406,20 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = [...command.slashes()] for (const serverCommand of sync.data.command) { + // kilocode_change start - preserve suffixes like :skill when inserting selected slash commands + const display = slashDisplay(serverCommand) results.push({ - display: slashDisplay(serverCommand), // kilocode_change + display, description: serverCommand.description, onSelect: () => { - const newText = "/" + serverCommand.name + " " + const newText = display + " " const cursor = props.input().logicalCursor props.input().deleteRange(0, 0, cursor.row, cursor.col) props.input().insertText(newText) props.input().cursorOffset = Bun.stringWidth(newText) }, }) + // kilocode_change end } results.sort((a, b) => a.display.localeCompare(b.display)) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 354f67f71db..0a3ea1ea13d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -58,6 +58,7 @@ import { import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" import { KiloSessionTuiSync } from "@/kilocode/session/tui-sync" // kilocode_change +import { slashMatches } from "@/kilocode/cli/cmd/command-display" // kilocode_change import { Flag } from "@opencode-ai/core/flag/flag" import { type WorkspaceStatus } from "../workspace-label" import { useCommandPalette } from "../../context/command-palette" @@ -1180,7 +1181,7 @@ export function Prompt(props: PromptProps) { iife(() => { const firstLine = inputText.split("\n")[0] const command = firstLine.split(" ")[0].slice(1) - return sync.data.command.some((x) => x.name === command) + return sync.data.command.some((x) => slashMatches(x, command)) // kilocode_change }) ) { // Parse command from first line, preserve multi-line content in arguments diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 3a19f1901f3..94703bfdc47 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -71,6 +71,28 @@ export interface Interface { readonly list: () => Effect.Effect } +// kilocode_change start - skills can share names with slash commands +function fromSkill(item: Skill.Info): Info { + return { + name: item.name, + description: item.description, + source: "skill", + get template() { + return item.content + }, + hints: [], + } +} + +function skillName(name: string) { + return name.endsWith(":skill") ? name.slice(0, -6) : undefined +} + +function mcpName(name: string) { + return name.endsWith(":mcp") ? name.slice(0, -4) : undefined +} +// kilocode_change end + export class Service extends Context.Service()("@opencode/Command") {} export const layer = Layer.effect( @@ -156,15 +178,7 @@ export const layer = Layer.effect( for (const item of yield* skill.all()) { if (commands[item.name]) continue - commands[item.name] = { - name: item.name, - description: item.description, - source: "skill", - get template() { - return item.content - }, - hints: [], - } + commands[item.name] = fromSkill(item) // kilocode_change } return { @@ -176,12 +190,38 @@ export const layer = Layer.effect( const get = Effect.fn("Command.get")(function* (name: string) { const s = yield* InstanceState.get(state) - return s.commands[name] + const exact = s.commands[name] + if (exact) return exact + + // kilocode_change start - route /name:skill to skills even when /name is a command + const target = skillName(name) + if (target) { + const item = yield* skill.get(target) + if (item) return fromSkill(item) + return undefined + } + // kilocode_change end + // kilocode_change start - allow autocomplete to insert displayed /name:mcp suffixes + const prompt = mcpName(name) + if (prompt) { + const cmd = s.commands[prompt] + return cmd?.source === "mcp" ? cmd : undefined + } + // kilocode_change end + return undefined }) const list = Effect.fn("Command.list")(function* () { const s = yield* InstanceState.get(state) - return Object.values(s.commands) + // kilocode_change start - include skill completions that conflict with commands + const result = Object.values(s.commands) + const names = new Set(result.map((item) => item.name)) + for (const item of yield* skill.all()) { + if (s.commands[item.name]?.source === "skill") continue + if (names.has(item.name)) result.push(fromSkill(item)) + } + return result + // kilocode_change end }) return Service.of({ get, list }) diff --git a/packages/opencode/src/kilocode/cli/cmd/command-display.ts b/packages/opencode/src/kilocode/cli/cmd/command-display.ts index 5a498fda61f..1194fd7aeb0 100644 --- a/packages/opencode/src/kilocode/cli/cmd/command-display.ts +++ b/packages/opencode/src/kilocode/cli/cmd/command-display.ts @@ -8,3 +8,7 @@ export function slashDisplay(cmd: Command) { if (cmd.source === "mcp") return `/${cmd.name}:mcp` return `/${cmd.name}` } + +export function slashMatches(cmd: Command, name: string) { + return cmd.name === name || slashDisplay(cmd).slice(1) === name +} diff --git a/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts new file mode 100644 index 00000000000..f7e81a786f4 --- /dev/null +++ b/packages/opencode/test/kilocode/skill-command-autocomplete.test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import path from "path" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Command } from "../../src/command" +import { provideTmpdirInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +const it = testEffect(Layer.mergeAll(Command.defaultLayer, CrossSpawnSpawner.defaultLayer)) + +describe("skill slash commands", () => { + it.live("lists and resolves skills that conflict with commands", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + yield* Effect.promise(() => + Bun.write( + path.join(dir, ".kilo", "skill", "review", "SKILL.md"), + `--- +name: review +description: Skill with command conflict. +--- + +# Review Skill + +Skill content. +`, + ), + ) + + const command = yield* Command.Service + const list = yield* command.list() + const matches = list.filter((item) => item.name === "review") + + expect(matches.some((item) => item.source === "command")).toBe(true) + expect(matches.some((item) => item.source === "skill")).toBe(true) + + const cmd = yield* command.get("review") + const skill = yield* command.get("review:skill") + + expect(cmd?.source).toBe("command") + expect(skill?.source).toBe("skill") + expect(yield* Effect.promise(async () => skill?.template)).toContain("Skill content.") + }), + { + git: true, + config: { + command: { + review: { + template: "Command content.", + }, + }, + }, + }, + ), + ) +}) From 48466850a9a6c3cfb75b6c93f71ff72eef35321d Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Thu, 4 Jun 2026 22:53:52 -0600 Subject: [PATCH 4/5] Undo extra changeset --- .changeset/fuzzy-skill-conflicts.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/fuzzy-skill-conflicts.md diff --git a/.changeset/fuzzy-skill-conflicts.md b/.changeset/fuzzy-skill-conflicts.md deleted file mode 100644 index 9cf1abec929..00000000000 --- a/.changeset/fuzzy-skill-conflicts.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Fix slash autocomplete and invocation for skills that share a name with commands. From c992b928420e53137d9b77d16ed7500267d449f3 Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Thu, 4 Jun 2026 23:20:33 -0600 Subject: [PATCH 5/5] fix(cli): annotate command Kilo changes --- packages/opencode/src/command/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 94703bfdc47..a2c69f4f000 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -190,10 +190,12 @@ export const layer = Layer.effect( const get = Effect.fn("Command.get")(function* (name: string) { const s = yield* InstanceState.get(state) + // kilocode_change start const exact = s.commands[name] if (exact) return exact + // kilocode_change end - // kilocode_change start - route /name:skill to skills even when /name is a command + // kilocode_change start const target = skillName(name) if (target) { const item = yield* skill.get(target) @@ -201,19 +203,19 @@ export const layer = Layer.effect( return undefined } // kilocode_change end - // kilocode_change start - allow autocomplete to insert displayed /name:mcp suffixes + // kilocode_change start const prompt = mcpName(name) if (prompt) { const cmd = s.commands[prompt] return cmd?.source === "mcp" ? cmd : undefined } // kilocode_change end - return undefined + return undefined // kilocode_change }) + // kilocode_change start const list = Effect.fn("Command.list")(function* () { const s = yield* InstanceState.get(state) - // kilocode_change start - include skill completions that conflict with commands const result = Object.values(s.commands) const names = new Set(result.map((item) => item.name)) for (const item of yield* skill.all()) { @@ -221,8 +223,8 @@ export const layer = Layer.effect( if (names.has(item.name)) result.push(fromSkill(item)) } return result - // kilocode_change end }) + // kilocode_change end return Service.of({ get, list }) }),