Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
955e18f
feat(vscode): add workflow management and slash command support
Githubguy132010 Feb 24, 2026
2de2bb3
fix(vscode): validate workflow names to prevent path traversal
Githubguy132010 Feb 24, 2026
a569f1e
fix(vscode): harden workflow name validation, prevent overwrite, simp…
Githubguy132010 Feb 24, 2026
8506487
fix(vscode): add confirmation dialog before workflow deletion
Githubguy132010 Feb 24, 2026
80e358f
Merge branch 'main' into feat/vscode-workflows-and-slash-commands
Githubguy132010 Feb 25, 2026
5efee6f
fix(vscode): scope workflow file ops and cloud preview slash commands
Githubguy132010 Feb 25, 2026
9ae78fa
Merge branch 'main' into feat/vscode-workflows-and-slash-commands
Githubguy132010 Feb 26, 2026
bf68b6d
fix(vscode): only run slash commands when command exists
Githubguy132010 Feb 26, 2026
28ea8dd
Merge branch 'main' into feat/vscode-workflows-and-slash-commands
Githubguy132010 Feb 27, 2026
73835cf
fix(vscode): handle cloud import and workflow lookup errors
Githubguy132010 Feb 27, 2026
e066930
Harden command validation and command loading fallback
Githubguy132010 Feb 27, 2026
3c8488b
fix(vscode): handle command load and import failures better
Githubguy132010 Feb 27, 2026
839c7b2
fix(vscode): type cloud import failure session id and optimistic slas…
Githubguy132010 Feb 27, 2026
b4ae76d
Update packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts
Githubguy132010 Feb 27, 2026
d8afb75
Prevent slash commands from dropping image attachments
Githubguy132010 Feb 27, 2026
1ca38ac
fix(vscode): set loading for sendCommand and cleanup optimistic messa…
Githubguy132010 Feb 27, 2026
306d46f
Fix slash command, workflow scope, and command loading edge cases
Githubguy132010 Feb 27, 2026
ddeaeb8
fix(vscode): workflow scope selection and session-aware command errors
Githubguy132010 Feb 27, 2026
d069c98
fix(vscode): remove dead sid guard and bound commands retries
Githubguy132010 Feb 27, 2026
2053b8f
fix: address open review issues in PR #6260
kilo-code-bot[bot] Feb 27, 2026
8a58712
fix: address follow-up review issues
kilo-code-bot[bot] Feb 27, 2026
2140c55
fix: address further review issues
kilo-code-bot[bot] Feb 27, 2026
06c5f45
fix: add sessionID to handleSendCommand error response
kilo-code-bot[bot] Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
454 changes: 454 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions packages/kilo-vscode/src/services/cli-backend/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
CloudSessionsResponse,
CloudSessionData,
EditorContext,
CommandInfo,
WorktreeFileDiff,
} from "./types"
import { extractHttpErrorMessage, parseSSEDataLine } from "./http-utils"
Expand Down Expand Up @@ -210,6 +211,40 @@ export class HttpClient {
return this.request<Config>("PATCH", "/global/config", config)
}

// ============================================
// Command/Workflow Methods
// ============================================

/**
* List all available commands (includes workflows, skills, MCP prompts).
*/
async listCommands(directory: string): Promise<CommandInfo[]> {
return this.request<CommandInfo[]>("GET", "/command", undefined, { directory })
}

/**
* Execute a command (workflow) in a session.
*/
async executeCommand(
sessionId: string,
command: string,
args: string,
directory: string,
options?: { agent?: string; model?: string },
): Promise<void> {
await this.request<void>(
"POST",
`/session/${sessionId}/command`,
{
command,
arguments: args,
agent: options?.agent,
model: options?.model,
},
{ directory, allowEmpty: true },
)
}

// ============================================
// Messaging Methods
// ============================================
Expand Down
9 changes: 9 additions & 0 deletions packages/kilo-vscode/src/services/cli-backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,15 @@ export interface CommandConfig {
description?: string
}

/** Command/workflow info returned by GET /command */
export interface CommandInfo {
name: string
description?: string
source?: "command" | "mcp" | "skill"
hints: string[]
workflowScope?: "project" | "global"
}

/** Skills configuration */
export interface SkillsConfig {
paths?: string[]
Expand Down
17 changes: 10 additions & 7 deletions packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { VSCodeProvider, useVSCode } from "../src/context/vscode"
import { ServerProvider } from "../src/context/server"
import { ProviderProvider } from "../src/context/provider"
import { ConfigProvider } from "../src/context/config"
import { CommandsProvider } from "../src/context/commands"
import { SessionProvider, useSession } from "../src/context/session"
import { WorktreeModeProvider } from "../src/context/worktree-mode"
import { ChatView } from "../src/components/chat"
Expand Down Expand Up @@ -2806,13 +2807,15 @@ export const AgentManagerApp: Component = () => {
<CodeComponentProvider component={Code}>
<ProviderProvider>
<ConfigProvider>
<SessionProvider>
<WorktreeModeProvider>
<DataBridge>
<AgentManagerContent />
</DataBridge>
</WorktreeModeProvider>
</SessionProvider>
<CommandsProvider>
<SessionProvider>
<WorktreeModeProvider>
<DataBridge>
<AgentManagerContent />
</DataBridge>
</WorktreeModeProvider>
</SessionProvider>
</CommandsProvider>
</ConfigProvider>
</ProviderProvider>
</CodeComponentProvider>
Expand Down
17 changes: 10 additions & 7 deletions packages/kilo-vscode/webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { VSCodeProvider, useVSCode } from "./context/vscode"
import { ServerProvider, useServer } from "./context/server"
import { ProviderProvider, useProvider } from "./context/provider"
import { ConfigProvider } from "./context/config"
import { CommandsProvider } from "./context/commands"
import { SessionProvider, useSession } from "./context/session"
import { LanguageProvider } from "./context/language"
import { ChatView } from "./components/chat"
Expand Down Expand Up @@ -260,13 +261,15 @@ const App: Component = () => {
<CodeComponentProvider component={Code}>
<ProviderProvider>
<ConfigProvider>
<NotificationsProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</NotificationsProvider>
<CommandsProvider>
<NotificationsProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</NotificationsProvider>
</CommandsProvider>
</ConfigProvider>
</ProviderProvider>
</CodeComponentProvider>
Expand Down
104 changes: 102 additions & 2 deletions packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Component, createSignal, createEffect, on, For, Index, onCleanup, Show,
import { Button } from "@kilocode/kilo-ui/button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { FileIcon } from "@kilocode/kilo-ui/file-icon"
import { showToast } from "@kilocode/kilo-ui/toast"
import { useSession } from "../../context/session"
import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
Expand All @@ -15,6 +16,8 @@ import { ModelSelector } from "../shared/ModelSelector"
import { ModeSwitcher } from "../shared/ModeSwitcher"
import { ThinkingSelector } from "../shared/ThinkingSelector"
import { useFileMention } from "../../hooks/useFileMention"
import { useSlashCommand } from "../../hooks/useSlashCommand"
import { useCommands } from "../../context/commands"
import { useImageAttachments } from "../../hooks/useImageAttachments"
import { fileName, dirName, buildHighlightSegments } from "./prompt-input-utils"

Expand All @@ -30,6 +33,8 @@ export const PromptInput: Component = () => {
const language = useLanguage()
const vscode = useVSCode()
const mention = useFileMention(vscode)
const { commands } = useCommands()
const slash = useSlashCommand(commands)
const imageAttach = useImageAttachments()

const sessionKey = () => session.currentSessionID() ?? "__new__"
Expand All @@ -40,6 +45,7 @@ export const PromptInput: Component = () => {
let textareaRef: HTMLTextAreaElement | undefined
let highlightRef: HTMLDivElement | undefined
let dropdownRef: HTMLDivElement | undefined
let slashDropdownRef: HTMLDivElement | undefined
let debounceTimer: ReturnType<typeof setTimeout> | undefined
let requestCounter = 0
// Save/restore input text when switching sessions.
Expand Down Expand Up @@ -154,6 +160,13 @@ export const PromptInput: Component = () => {
if (active) active.scrollIntoView({ block: "nearest" })
}

const scrollToActiveSlashItem = () => {
if (!slashDropdownRef) return
const items = slashDropdownRef.querySelectorAll(".slash-command-item")
const active = items[slash.index()] as HTMLElement | undefined
if (active) active.scrollIntoView({ block: "nearest" })
}

const syncHighlightScroll = () => {
if (highlightRef && textareaRef) {
highlightRef.scrollTop = textareaRef.scrollTop
Expand Down Expand Up @@ -186,8 +199,9 @@ export const PromptInput: Component = () => {
syncHighlightScroll()

mention.onInput(val, target.selectionStart ?? val.length)
slash.onInput(val)

if (mention.showMention()) {
if (mention.showMention() || slash.show()) {
setGhostText("")
if (debounceTimer) clearTimeout(debounceTimer)
return
Expand All @@ -204,6 +218,12 @@ export const PromptInput: Component = () => {
return
}

if (slash.onKeyDown(e, textareaRef, setText, adjustHeight)) {
setGhostText("")
queueMicrotask(scrollToActiveSlashItem)
return
}

if ((e.key === "Tab" || e.key === "ArrowRight") && ghostText()) {
e.preventDefault()
acceptSuggestion()
Expand All @@ -229,10 +249,45 @@ export const PromptInput: Component = () => {
}

const handleSend = () => {
const message = text().trim()
const raw = text().trim()
const escaped = raw.startsWith("\\/") ? raw.slice(1) : raw
const message = escaped
const imgs = imageAttach.images()
if ((!message && imgs.length === 0) || isBusy() || isDisabled()) return

if (!raw.startsWith("\\/") && message.startsWith("/")) {
const parts = message.slice(1).split(/\s+/)
const command = parts[0]
const args = parts.slice(1).join(" ")
// kilocode_change start
const known = commands().some((cmd) => cmd.name === command)
if (command && known) {
if (imgs.length > 0) {
showToast({
variant: "warning",
title: "Slash command sent as regular message",
description:
"Slash commands do not support image attachments yet, so this input was sent as plain text to preserve attached images.",
})
}
if (imgs.length === 0) {
const sel = session.selected()
session.sendCommand(command, args, sel?.providerID, sel?.modelID)
requestCounter++
setText("")
setGhostText("")
imageAttach.clear()
if (debounceTimer) clearTimeout(debounceTimer)
slash.close()
mention.closeMention()
drafts.delete(sessionKey())
if (textareaRef) textareaRef.style.height = "auto"
return
}
}
// kilocode_change end
}

const mentionFiles = mention.parseFileAttachments(message)
const imgFiles = imgs.map((img) => ({ mime: img.mime, url: img.dataUrl }))
const allFiles = [...mentionFiles, ...imgFiles]
Expand All @@ -248,6 +303,7 @@ export const PromptInput: Component = () => {
imageAttach.clear()
if (debounceTimer) clearTimeout(debounceTimer)
mention.closeMention()
slash.close()
drafts.delete(sessionKey())

if (textareaRef) textareaRef.style.height = "auto"
Expand Down Expand Up @@ -287,6 +343,50 @@ export const PromptInput: Component = () => {
</Show>
</div>
</Show>
<Show when={slash.show()}>
<div class="slash-command-dropdown" ref={slashDropdownRef}>
<Show
when={slash.filtered().length > 0}
fallback={<div class="slash-command-empty">{language.t("prompt.popover.emptyCommands")}</div>}
>
<For each={slash.filtered()}>
{(cmd, index) => (
<div
class="slash-command-item"
classList={{ "slash-command-item--active": index() === slash.index() }}
onMouseDown={(e) => {
e.preventDefault()
const newText = `/${cmd.name} `
setText(newText)
if (textareaRef) {
textareaRef.value = newText
textareaRef.setSelectionRange(newText.length, newText.length)
textareaRef.focus()
}
slash.close()
adjustHeight()
}}
onMouseEnter={() => slash.setIndex(index())}
>
<span class="slash-command-name">/{cmd.name}</span>
<Show when={cmd.description}>
<span class="slash-command-description">{cmd.description}</span>
</Show>
<Show when={cmd.source && cmd.source !== "command"}>
<span class="slash-command-badge">
{cmd.source === "skill"
? language.t("prompt.slash.badge.skill")
: cmd.source === "mcp"
? language.t("prompt.slash.badge.mcp")
: language.t("prompt.slash.badge.custom")}
</span>
</Show>
</div>
)}
</For>
</Show>
</div>
</Show>
<Show when={imageAttach.images().length > 0}>
<div class="image-attachments">
<For each={imageAttach.images()}>
Expand Down
Loading
Loading