diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index ddb857d3dda..1aa37581214 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,4 +1,6 @@ import * as path from "path" +import * as fs from "fs" +import * as os from "os" import * as vscode from "vscode" import { z } from "zod" import { @@ -39,6 +41,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private cachedAgentsMessage: unknown = null /** Cached configLoaded payload so requestConfig can be served before httpClient is ready */ private cachedConfigMessage: unknown = null + /** Cached commandsLoaded payload so requestCommands can be served before httpClient is ready */ + private cachedCommandsMessage: unknown = null /** Cached notificationsLoaded payload */ private cachedNotificationsMessage: unknown = null @@ -365,6 +369,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.handleOpenFile(message.filePath, message.line, message.column) } break + case "sendCommand": + await this.handleSendCommand( + message.command, + message.arguments, + message.sessionID, + message.providerID, + message.modelID, + message.agent, + ) + break + case "importAndSendCommand": + await this.handleImportAndSendCommand( + message.cloudSessionId, + message.command, + message.arguments, + message.providerID, + message.modelID, + message.agent, + ) + break + case "openWorkflowFile": + await this.handleOpenWorkflowFile(message.name, message.workflowScope) + break + case "createWorkflowFile": + await this.handleCreateWorkflowFile(message.name, message.workflowScope) + break + case "deleteWorkflowFile": + await this.handleDeleteWorkflowFile(message.name, message.workflowScope) + break case "requestProviders": this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e)) break @@ -383,6 +416,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "requestConfig": this.fetchAndSendConfig().catch((e) => console.error("[Kilo New] fetchAndSendConfig failed:", e)) break + case "requestCommands": + this.fetchAndSendCommands().catch((e) => console.error("[Kilo New] fetchAndSendCommands failed:", e)) + break case "updateConfig": await this.handleUpdateConfig(message.config) break @@ -602,6 +638,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.fetchAndSendProviders(), this.fetchAndSendAgents(), this.fetchAndSendConfig(), + this.fetchAndSendCommands(), this.fetchAndSendNotifications(), ]) this.sendNotificationSettings() @@ -1028,6 +1065,51 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + /** + * Fetch available commands (workflows, skills, MCP prompts) and send to webview. + */ + private async fetchAndSendCommands(): Promise { + if (!this.httpClient) { + if (this.cachedCommandsMessage) { + this.postMessage(this.cachedCommandsMessage) + } + return + } + + try { + const workspaceDir = this.getWorkspaceDirectory() + const commands = await this.httpClient.listCommands(workspaceDir) + const enriched = await Promise.all( + commands.map(async (cmd) => ({ + ...cmd, + workflowScope: await this.getWorkflowScope(cmd.name, cmd.source), + })), + ) + + const message = { + type: "commandsLoaded", + commands: enriched.map((cmd) => ({ + name: cmd.name, + description: cmd.description, + source: cmd.source, + hints: cmd.hints, + workflowScope: cmd.workflowScope, + })), + } + this.cachedCommandsMessage = message + this.postMessage(message) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to fetch commands:", error) + const message = { + type: "commandsLoaded", + commands: [], + error: error instanceof Error ? error.message : "Failed to fetch commands", + } + this.cachedCommandsMessage = message + this.postMessage(message) + } + } + /** * Fetch Kilo news/notifications and send to webview. * Uses the cached message pattern so the webview gets data immediately on refresh. @@ -1286,6 +1368,378 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + /** + * Handle command execution request from the webview. + * Routes "/command args" to the session command endpoint. + */ + private async handleSendCommand( + command: string, + args: unknown, + sessionID?: string, + providerID?: string, + modelID?: string, + agent?: string, + ): Promise { + if (!this.httpClient) { + this.postMessage({ type: "error", message: "Not connected to CLI backend" }) + return + } + + try { + const workspaceDir = this.getWorkspaceDirectory(sessionID || this.currentSession?.id) + const validated = this.validateCommandName(command) + if (!validated) { + throw new Error("Invalid command") + } + const validatedArgs = this.validateCommandArgs(args) + if (validatedArgs === null) { + throw new Error("Invalid command arguments") + } + + // Create session if needed + if (!sessionID && !this.currentSession) { + this.currentSession = await this.httpClient.createSession(workspaceDir) + this.trackedSessionIds.add(this.currentSession.id) + this.postMessage({ + type: "sessionCreated", + session: this.sessionToWebview(this.currentSession), + }) + } + + const target = sessionID || this.currentSession?.id + if (!target) { + throw new Error("No session available") + } + + const model = providerID && modelID ? `${providerID}/${modelID}` : undefined + + await this.httpClient.executeCommand(target, validated, validatedArgs, workspaceDir, { agent, model }) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to execute command:", error) + const session = sessionID || this.currentSession?.id + const base = error instanceof Error ? error.message : "Failed to execute command" + // kilocode_change: include sessionID property so the webview error handler + // can match the correct session instead of always matching the current one + this.postMessage({ + type: "error", + message: session ? `[sessionID:${session}] ${base}` : base, + sessionID: session, + }) + } + } + + /** + * Import a cloud session to local storage, then execute a command on it. + */ + private async handleImportAndSendCommand( + cloudSessionId: string, + command: string, + args: unknown, + providerID?: string, + modelID?: string, + agent?: string, + ): Promise { + if (!this.httpClient) { + this.postMessage({ + type: "cloudSessionImportFailed", + cloudSessionId, + error: "Not connected to CLI backend", + }) + return + } + + const workspaceDir = this.getWorkspaceDirectory() + const validated = this.validateCommandName(command) + if (!validated) { + this.postMessage({ + type: "cloudSessionImportFailed", + cloudSessionId, + error: "Invalid command", + }) + return + } + const validatedArgs = this.validateCommandArgs(args) + if (validatedArgs === null) { + this.postMessage({ + type: "cloudSessionImportFailed", + cloudSessionId, + error: "Invalid command arguments", + }) + return + } + + let session: Awaited> + try { + session = await this.httpClient.importCloudSession(cloudSessionId, workspaceDir) + } catch (err) { + this.postMessage({ + type: "cloudSessionImportFailed", + cloudSessionId, + error: err instanceof Error ? err.message : "Failed to import session from cloud", + }) + return + } + + if (!session) { + this.postMessage({ + type: "cloudSessionImportFailed", + cloudSessionId, + error: "Failed to import session from cloud", + }) + return + } + + this.currentSession = session + this.trackedSessionIds.add(session.id) + + this.postMessage({ + type: "cloudSessionImported", + cloudSessionId, + session: this.sessionToWebview(session), + }) + + try { + const model = providerID && modelID ? `${providerID}/${modelID}` : undefined + await this.httpClient.executeCommand(session.id, validated, validatedArgs, workspaceDir, { + agent, + model, + }) + } catch (err) { + console.error("[Kilo New] Failed to execute command after cloud import:", err) + // kilocode_change: the import already succeeded — send a regular error so the + // webview keeps the imported session instead of discarding it via cloudSessionImportFailed + const base = err instanceof Error ? err.message : "Failed to execute command after import" + this.postMessage({ + type: "error", + message: `[sessionID:${session.id}] ${base}`, + sessionID: session.id, + }) + } + } + + private getProjectWorkflowsDir(): string { + return path.join(this.getWorkspaceDirectory(), ".kilocode", "workflows") + } + + private getGlobalWorkflowsDir(): string { + const fromExtension = this.extensionContext?.globalStorageUri.fsPath + if (fromExtension) { + return path.join(fromExtension, "workflows") + } + return path.join(os.homedir(), ".kilocode", "workflows") + } + + private async fileExists(filePath: string): Promise { + return fs.promises + .access(filePath, fs.constants.F_OK) + .then(() => true) + .catch(() => false) + } + + private getGlobalWorkflowCandidates(name: string): string[] { + const file = `${name}.md` + const globalStorage = this.extensionContext?.globalStorageUri.fsPath + const result = [path.join(os.homedir(), ".kilocode", "workflows", file)] + if (globalStorage) { + result.unshift(path.join(globalStorage, "workflows", file)) + } + return result + } + + private validateCommandName(name: string): string | null { + const command = typeof name === "string" ? name.trim() : "" + if (!command) return null + if (!/^[a-zA-Z0-9_-]+$/.test(command)) return null + return command + } + + private validateCommandArgs(args: unknown): string | null { + if (typeof args !== "string") return null + if (args.length > 100_000) return null + return args + } + + private async getWorkflowScope( + name: string, + source?: "command" | "mcp" | "skill", + ): Promise<"project" | "global" | undefined> { + if (source !== "command") return undefined + const validated = this.validateWorkflowName(name) + if (!validated) return undefined + + const projectPath = path.join(this.getProjectWorkflowsDir(), `${validated}.md`) + if (await this.fileExists(projectPath)) { + return "project" + } + + const globalCandidates = this.getGlobalWorkflowCandidates(validated) + const exists = await Promise.all(globalCandidates.map((candidate) => this.fileExists(candidate))) + if (exists.some(Boolean)) { + return "global" + } + + return undefined + } + + private async resolveWorkflowFilePath( + name: string, + scope?: "project" | "global", + ): Promise<{ path: string; scope: "project" | "global" } | null> { + const project = path.join(this.getProjectWorkflowsDir(), `${name}.md`) + const globals = this.getGlobalWorkflowCandidates(name) + const targets = + scope === "project" + ? [{ path: project, scope: "project" as const }] + : scope === "global" + ? globals.map((filePath) => ({ path: filePath, scope: "global" as const })) + : [{ path: project, scope: "project" as const }, ...globals.map((filePath) => ({ path: filePath, scope: "global" as const }))] + + for (const target of targets) { + if (!(await this.fileExists(target.path))) { + continue + } + return target + } + return null + } + + /** + * Validate a workflow name to prevent path traversal attacks. + * Only allows alphanumeric characters, hyphens, and underscores. + * Returns the sanitized name, or null if invalid. + */ + private validateWorkflowName(name: string): string | null { + const sanitized = path.basename(name).replace(/\.md$/, "") + if (!sanitized || sanitized !== name || !/^[a-zA-Z0-9_-]+$/.test(sanitized)) return null + return sanitized + } + + /** + * Open a workflow file in the editor. + */ + private async handleOpenWorkflowFile(name: string, scope?: "project" | "global"): Promise { + const validated = this.validateWorkflowName(name) + if (!validated) { + this.postMessage({ type: "error", message: "Invalid workflow name" }) + return + } + + const workflow = await this.resolveWorkflowFilePath(validated, scope) + if (!workflow) { + this.postMessage({ type: "error", message: `Workflow \"${validated}\" file not found` }) + return + } + + try { + const uri = vscode.Uri.file(workflow.path) + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to open workflow file:", error) + this.postMessage({ + type: "error", + message: error instanceof Error ? error.message : "Failed to open workflow file", + }) + } + } + + /** + * Create a new workflow file and open it in the editor. + */ + private async handleCreateWorkflowFile(name: string, scope?: "project" | "global"): Promise { + const validated = this.validateWorkflowName(name) + if (!validated) { + this.postMessage({ type: "error", message: "Invalid workflow name" }) + return + } + + const existing = await this.resolveWorkflowFilePath(validated, scope) + if (existing) { + this.postMessage({ + type: "error", + message: `Workflow \"${validated}\" already exists in ${existing.scope} scope`, + }) + return + } + + const selected = + scope ?? + (await vscode.window.showQuickPick( + [ + { label: "Project", value: "project" as const }, + { label: "Global", value: "global" as const }, + ], + { + placeHolder: `Choose scope for new workflow \"${validated}\"`, + ignoreFocusOut: true, + }, + ))?.value + if (!selected) return + + const dir = + selected === "global" + ? this.getGlobalWorkflowsDir() + : this.getProjectWorkflowsDir() + const filePath = path.join(dir, `${validated}.md`) + + try { + // Ensure directory exists + await fs.promises.mkdir(dir, { recursive: true }) + + // Create template content + const template = `# ${validated}\n\nDescribe your workflow here.\n` + await fs.promises.writeFile(filePath, template, { encoding: "utf-8", flag: "wx" }) + + // Open in editor + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)) + await vscode.window.showTextDocument(doc) + + // Refresh commands so the new workflow appears + await this.fetchAndSendCommands() + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to create workflow file:", error) + this.postMessage({ + type: "error", + message: error instanceof Error ? error.message : "Failed to create workflow file", + }) + } + } + + /** + * Delete a workflow file and refresh the commands list. + */ + private async handleDeleteWorkflowFile(name: string, scope?: "project" | "global"): Promise { + const validated = this.validateWorkflowName(name) + if (!validated) { + this.postMessage({ type: "error", message: "Invalid workflow name" }) + return + } + + const confirmed = await vscode.window.showWarningMessage( + `Delete workflow "${validated}"?`, + { modal: true }, + "Delete", + ) + if (confirmed !== "Delete") return + + const workflow = await this.resolveWorkflowFilePath(validated, scope) + if (!workflow) { + this.postMessage({ type: "error", message: `Workflow \"${validated}\" file not found` }) + return + } + + try { + await fs.promises.unlink(workflow.path) + await this.fetchAndSendCommands() + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to delete workflow file:", error) + this.postMessage({ + type: "error", + message: error instanceof Error ? error.message : "Failed to delete workflow file", + }) + } + } + /** * Handle sending a message from the webview. */ diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index cadb342791f..d72d2c034ed 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -15,6 +15,7 @@ import type { CloudSessionsResponse, CloudSessionData, EditorContext, + CommandInfo, WorktreeFileDiff, } from "./types" import { extractHttpErrorMessage, parseSSEDataLine } from "./http-utils" @@ -210,6 +211,40 @@ export class HttpClient { return this.request("PATCH", "/global/config", config) } + // ============================================ + // Command/Workflow Methods + // ============================================ + + /** + * List all available commands (includes workflows, skills, MCP prompts). + */ + async listCommands(directory: string): Promise { + return this.request("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 { + await this.request( + "POST", + `/session/${sessionId}/command`, + { + command, + arguments: args, + agent: options?.agent, + model: options?.model, + }, + { directory, allowEmpty: true }, + ) + } + // ============================================ // Messaging Methods // ============================================ diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index 0a6fc135fe4..1d756b2e6e6 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -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[] diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 1f55120cdf6..e1b389ec922 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -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" @@ -2806,13 +2807,15 @@ export const AgentManagerApp: Component = () => { - - - - - - - + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index d466bb1960f..d09d3d8e3ea 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -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" @@ -260,13 +261,15 @@ const App: Component = () => { - - - - - - - + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 96a12d0556e..d61f5faabbf 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -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" @@ -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" @@ -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__" @@ -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 | undefined let requestCounter = 0 // Save/restore input text when switching sessions. @@ -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 @@ -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 @@ -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() @@ -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] @@ -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" @@ -287,6 +343,50 @@ export const PromptInput: Component = () => { + +
+ 0} + fallback={
{language.t("prompt.popover.emptyCommands")}
} + > + + {(cmd, index) => ( +
{ + 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())} + > + /{cmd.name} + + {cmd.description} + + + + {cmd.source === "skill" + ? language.t("prompt.slash.badge.skill") + : cmd.source === "mcp" + ? language.t("prompt.slash.badge.mcp") + : language.t("prompt.slash.badge.custom")} + + +
+ )} +
+
+
+
0}>
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index 08eb04c9563..d8ef8009950 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -5,6 +5,8 @@ import { Card } from "@kilocode/kilo-ui/card" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { useCommands } from "../../context/commands" +import { useVSCode } from "../../context/vscode" import { useConfig } from "../../context/config" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" @@ -56,6 +58,9 @@ const AgentBehaviourTab: Component = () => { const [newSkillPath, setNewSkillPath] = createSignal("") const [newSkillUrl, setNewSkillUrl] = createSignal("") const [newInstruction, setNewInstruction] = createSignal("") + const { commands } = useCommands() + const vscode = useVSCode() + const [newWorkflowName, setNewWorkflowName] = createSignal("") const agentNames = createMemo(() => { const names = session.agents().map((a) => a.name) @@ -517,6 +522,141 @@ const AgentBehaviourTab: Component = () => {
) + const renderWorkflowsSubtab = () => { + const items = createMemo(() => commands().filter((cmd) => cmd.source === "command" && !!cmd.workflowScope)) + + const createWorkflow = () => { + const name = newWorkflowName() + .trim() + .replace(/\.md$/i, "") + .replace(/[^a-zA-Z0-9_-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") + if (!name) return + vscode.postMessage({ type: "createWorkflowFile", name }) + setNewWorkflowName("") + } + + return ( +
+ +
+
{language.t("settings.agentBehaviour.workflows.title")}
+
+ {language.t("settings.agentBehaviour.workflows.description")} +
+
+ +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+ setNewWorkflowName(val)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") createWorkflow() + }} + /> +
+ +
+ + 0} + fallback={ +
+ {language.t("settings.agentBehaviour.workflowsEmpty")} +
+ } + > + + {(cmd, index) => ( +
+
+
{cmd.name}
+ +
+ {cmd.description} +
+
+
+
+ + vscode.postMessage({ + type: "openWorkflowFile", + name: cmd.name, + workflowScope: cmd.workflowScope, + }) + } + /> + + vscode.postMessage({ + type: "deleteWorkflowFile", + name: cmd.name, + workflowScope: cmd.workflowScope, + }) + } + /> +
+
+ )} +
+
+
+
+ ) + } + const renderSubtabContent = () => { switch (activeSubtab()) { case "agents": @@ -526,7 +666,7 @@ const AgentBehaviourTab: Component = () => { case "rules": return renderRulesSubtab() case "workflows": - return + return renderWorkflowsSubtab() case "skills": return renderSkillsSubtab() default: diff --git a/packages/kilo-vscode/webview-ui/src/context/commands.tsx b/packages/kilo-vscode/webview-ui/src/context/commands.tsx new file mode 100644 index 00000000000..c1aae9069ce --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/commands.tsx @@ -0,0 +1,114 @@ +/** + * Commands context + * Fetches and exposes the list of available commands (workflows, skills, MCP prompts) + * from the CLI backend. + */ + +import { createContext, useContext, createSignal, onCleanup, ParentComponent, Accessor } from "solid-js" +import { showToast } from "@kilocode/kilo-ui/toast" +import { useVSCode } from "./vscode" +import type { CommandInfo, ExtensionMessage } from "../types/messages" + +interface CommandsContextValue { + commands: Accessor + loading: Accessor + error: Accessor +} + +const CommandsContext = createContext() + +export const CommandsProvider: ParentComponent = (props) => { + const vscode = useVSCode() + + const [commands, setCommands] = createSignal([]) + const [loading, setLoading] = createSignal(true) + const [error, setError] = createSignal() + + // kilocode_change start + const baseMs = 500 + const maxMs = 5000 + const maxRetries = 12 + const timeouts = new Set() + + const clearTimeouts = () => { + for (const timeout of timeouts) { + clearTimeout(timeout) + } + timeouts.clear() + } + + const tick = (delay: number, retry: number) => { + const timeout = setTimeout(() => { + timeouts.delete(timeout) + if (!loading()) return + if (retry >= maxRetries) { + const msg = "Timed out loading commands" + setError(msg) + setLoading(false) + showToast({ + variant: "error", + title: "Failed to load commands", + description: msg, + }) + return + } + vscode.postMessage({ type: "requestCommands" }) + tick(Math.min(delay * 2, maxMs), retry + 1) + }, delay) + timeouts.add(timeout) + } + + const startRetry = () => { + clearTimeouts() + setLoading(true) + setError(undefined) + vscode.postMessage({ type: "requestCommands" }) + tick(baseMs, 1) + } + + const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { + if (message.type === "commandsLoaded") { + clearTimeouts() + setCommands(message.commands) + setError(message.error) + setLoading(false) + if (message.error) { + showToast({ + variant: "error", + title: "Failed to load commands", + description: message.error, + }) + } + return + } + // Re-request commands when backend (re)connects, in case the retry window expired + if (message.type === "ready" || (message.type === "connectionState" && message.state === "connected")) { + if (loading() || error()) { + startRetry() + } + } + }) + + onCleanup(unsubscribe) + + startRetry() + + onCleanup(clearTimeouts) + // kilocode_change end + + const value: CommandsContextValue = { + commands, + loading, + error, + } + + return {props.children} +} + +export function useCommands(): CommandsContextValue { + const context = useContext(CommandsContext) + if (!context) { + throw new Error("useCommands must be used within a CommandsProvider") + } + return context +} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 82962c4a9eb..88e73c73d24 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -120,6 +120,7 @@ interface SessionContextValue { // Actions sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void + sendCommand: (command: string, args: string, providerID?: string, modelID?: string) => void abort: () => void compact: () => void respondToPermission: (permissionId: string, response: "once" | "always" | "reject") => void @@ -191,6 +192,11 @@ export const SessionProvider: ParentComponent = (props) => { // Cloud session preview state const [cloudPreviewId, setCloudPreviewId] = createSignal(null) + // kilocode_change start + // Temporary session key used for optimistic messages before a real session is created + const [pendingSessionKey, setPendingSessionKey] = createSignal(null) + // kilocode_change end + // Store for sessions, messages, parts, todos, modelSelections, agentSelections const [store, setStore] = createStore({ sessions: {}, @@ -390,7 +396,11 @@ export const SessionProvider: ParentComponent = (props) => { case "error": // Only clear loading if the error is for the current session // (or has no sessionID for backwards compatibility) - if (!message.sessionID || message.sessionID === currentSessionID()) setLoading(false) + clearOptimisticMessages(message.sessionID ?? currentSessionID()) // kilocode_change + if (!message.sessionID || message.sessionID === currentSessionID()) { + setLoading(false) + setPendingSessionKey(null) // kilocode_change: reset stale pending key on error + } break case "cloudSessionDataLoaded": @@ -402,8 +412,13 @@ export const SessionProvider: ParentComponent = (props) => { break case "cloudSessionImportFailed": + const key = `cloud:${message.cloudSessionId}` + clearOptimisticMessages(key) + const active = currentSessionID() setCloudPreviewId(null) - setCurrentSessionID(undefined) + if (!message.sessionID || active === key) { + setCurrentSessionID(undefined) + } setLoading(false) showToast({ variant: "error", @@ -418,11 +433,44 @@ export const SessionProvider: ParentComponent = (props) => { onCleanup(unsubscribe) }) + function clearOptimisticMessages(sessionID: string | undefined) { + if (!sessionID) return + const all = store.messages[sessionID] ?? [] + const ids = new Set(all.filter((m) => m.id.startsWith("optimistic-")).map((m) => m.id)) + if (ids.size === 0) return + const next = all.filter((m) => !ids.has(m.id)) + setStore("messages", sessionID, next) + setStore( + "parts", + produce((parts) => { + for (const id of ids) { + delete parts[id] + } + }), + ) + } + // Event handlers function handleSessionCreated(session: SessionInfo) { batch(() => { setStore("sessions", session.id, session) + // kilocode_change start + // Transfer optimistic messages from the pending session key (created before + // the real session ID was known) to the real session ID, then clean up the + // stale pending entry to avoid a memory leak. + const pendingKey = pendingSessionKey() + if (pendingKey) { + const pendingMessages = store.messages[pendingKey] ?? [] + if (pendingMessages.length > 0) { + setStore("messages", session.id, pendingMessages) + } + // Remove the stale pending entry from the store + setStore("messages", { [pendingKey]: undefined }) + setPendingSessionKey(null) + } + // kilocode_change end + // Only initialize messages if none exist yet — a cloud session import // (handleCloudSessionImported) may have already populated messages for // this session ID. The SSE session.created event can race with the @@ -799,6 +847,7 @@ export const SessionProvider: ParentComponent = (props) => { const preview = cloudPreviewId() if (preview) { + setLoading(true) // kilocode_change const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined vscode.postMessage({ type: "importAndSend", @@ -842,6 +891,85 @@ export const SessionProvider: ParentComponent = (props) => { }) } + function sendCommand(command: string, args: string, providerID?: string, modelID?: string) { + if (!server.isConnected()) { + console.warn("[Kilo New] Cannot send command: not connected") + return + } + + const preview = cloudPreviewId() + if (preview) { + setLoading(true) // kilocode_change + const sid = `cloud:${preview}` + const text = args.trim() ? `/${command} ${args}` : `/${command}` + const tempId = `optimistic-${crypto.randomUUID()}` + const now = Date.now() + const temp: Message = { + id: tempId, + sessionID: sid, + role: "user", + createdAt: new Date(now).toISOString(), + time: { created: now }, + } + setStore("messages", sid, (msgs = []) => [...msgs, temp]) + setStore("parts", tempId, [{ type: "text" as const, id: `${tempId}-text`, text }]) + + const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined + vscode.postMessage({ + type: "importAndSendCommand", + cloudSessionId: preview, + command, + arguments: args, + providerID, + modelID, + agent, + }) + return + } + + // kilocode_change start + // A pending:* key means a session is being created but hasn't been confirmed yet. + // Treat it as "no real session" so we don't send a synthetic key to the backend. + const rawSid = currentSessionID() + const sid = rawSid?.startsWith("pending:") ? undefined : rawSid + setLoading(true) // kilocode_change + const text = args.trim() ? `/${command} ${args}` : `/${command}` + // Create an optimistic message immediately so the user sees their command in the + // chat without waiting for the server. When there is no session yet, use a + // temporary pending key; handleSessionCreated will transfer the messages once + // the real session ID is known. + const existingPending = pendingSessionKey() + const targetSid = sid ?? existingPending ?? `pending:${crypto.randomUUID()}` + if (!sid && !existingPending) { + setPendingSessionKey(targetSid) + setCurrentSessionID(targetSid) + } + const tempId = `optimistic-${crypto.randomUUID()}` + const now = Date.now() + const temp: Message = { + id: tempId, + sessionID: targetSid, + role: "user", + createdAt: new Date(now).toISOString(), + time: { created: now }, + } + setStore("messages", targetSid, (msgs = []) => [...msgs, temp]) + setStore("parts", tempId, [{ type: "text" as const, id: `${tempId}-text`, text }]) + // kilocode_change end + + const agent = selectedAgentName() !== defaultAgent() ? selectedAgentName() : undefined + + vscode.postMessage({ + type: "sendCommand", + command, + arguments: args, + sessionID: sid, + providerID, + modelID, + agent, + }) + } + function abort() { const sessionID = currentSessionID() if (!sessionID) { @@ -1102,6 +1230,7 @@ export const SessionProvider: ParentComponent = (props) => { currentVariant, selectVariant, sendMessage, + sendCommand, abort, compact, respondToPermission, diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts new file mode 100644 index 00000000000..390f0f5413a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -0,0 +1,118 @@ +/** + * Hook for slash command detection and selection in the chat input. + * When the user types "/" at the start of the input, shows a filtered + * list of available commands (workflows, skills, MCP prompts). + */ + +import { createSignal, createMemo } from "solid-js" +import type { Accessor } from "solid-js" +import type { CommandInfo } from "../types/messages" + +export interface SlashCommand { + show: Accessor + filtered: Accessor + index: Accessor + setIndex: (i: number) => void + onInput: (val: string) => void + onKeyDown: ( + e: KeyboardEvent, + textarea: HTMLTextAreaElement | undefined, + setText: (text: string) => void, + onSelect?: () => void, + ) => boolean + close: () => void +} + +export function useSlashCommand(commands: Accessor): SlashCommand { + const [query, setQuery] = createSignal(null) + const [index, setIndex] = createSignal(0) + + const show = () => query() !== null + + const filtered = createMemo(() => { + const q = query() + if (q === null) return [] + const lower = q.toLowerCase() + return commands().filter( + (cmd) => cmd.name.toLowerCase().includes(lower) || (cmd.description ?? "").toLowerCase().includes(lower), + ) + }) + + const close = () => { + setQuery(null) + setIndex(0) + } + + const onInput = (val: string) => { + if (val.startsWith("/") && !val.slice(1).includes(" ")) { + setQuery(val.slice(1)) + setIndex(0) + return + } + close() + } + + const select = ( + cmd: CommandInfo, + textarea: HTMLTextAreaElement | undefined, + setText: (text: string) => void, + onSelect?: () => void, + ) => { + const newText = `/${cmd.name} ` + setText(newText) + if (textarea) { + textarea.value = newText + const cursor = newText.length + textarea.setSelectionRange(cursor, cursor) + textarea.focus() + } + close() + onSelect?.() + } + + const onKeyDown = ( + e: KeyboardEvent, + textarea: HTMLTextAreaElement | undefined, + setText: (text: string) => void, + onSelect?: () => void, + ): boolean => { + if (!show()) return false + + if (e.key === "ArrowDown") { + e.preventDefault() + const list = filtered() + if (list.length === 0) return true + setIndex((i) => Math.min(i + 1, list.length - 1)) + return true + } + if (e.key === "ArrowUp") { + e.preventDefault() + setIndex((i) => Math.max(i - 1, 0)) + return true + } + if (e.key === "Enter" || e.key === "Tab") { + const cmd = filtered()[index()] + if (!cmd) return false + e.preventDefault() + select(cmd, textarea, setText, onSelect) + return true + } + if (e.key === "Escape") { + e.preventDefault() + close() + return true + } + + return false + } + + return { + show, + filtered, + index, + setIndex, + onInput, + onKeyDown, + close, + } +} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 1547c271f14..fc2171e1886 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -892,6 +892,12 @@ export const dict = { "settings.agentBehaviour.instructionFiles.description": "مسارات ملفات التعليمات الإضافية في موجه النظام", "settings.agentBehaviour.mcpEmpty": "لم يتم تهيئة خوادم MCP. قم بتحرير ملف تهيئة opencode لإضافة خوادم MCP.", "settings.agentBehaviour.workflowsPlaceholder": "تُدار سير العمل عبر ملفات سير العمل في مساحة العمل.", + "settings.agentBehaviour.workflowsEmpty": + "لم يتم العثور على سير عمل. أضف ملفات .md إلى .kilocode/workflows/ في مشروعك أو ~/.kilocode/workflows/ بشكل عام.", + "settings.agentBehaviour.workflows.title": "سير العمل", + "settings.agentBehaviour.workflows.description": + "سير العمل هي ملفات .md في .kilocode/workflows/. استخدم /name في الدردشة لتشغيلها.", + "settings.agentBehaviour.workflows.namePlaceholder": "مثال: deploy, test-suite", "settings.agentBehaviour.notImplemented": "لم يتم التنفيذ بعد.", "settings.autoApprove.setAll": "تعيين جميع الأذونات", "settings.autoApprove.level.allow": "سماح", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index ea5d29f969f..a82b91e4655 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -906,6 +906,12 @@ export const dict = { "Nenhum servidor MCP configurado. Edite o arquivo de configuração do opencode para adicionar servidores MCP.", "settings.agentBehaviour.workflowsPlaceholder": "Fluxos de trabalho são gerenciados por arquivos de fluxo de trabalho no espaço de trabalho.", + "settings.agentBehaviour.workflowsEmpty": + "Nenhum fluxo de trabalho encontrado. Adicione arquivos .md em .kilocode/workflows/ no seu projeto ou ~/.kilocode/workflows/ globalmente.", + "settings.agentBehaviour.workflows.title": "Fluxos de trabalho", + "settings.agentBehaviour.workflows.description": + "Fluxos de trabalho são arquivos .md em .kilocode/workflows/. Use /nome no chat para executá-los.", + "settings.agentBehaviour.workflows.namePlaceholder": "ex: deploy, test-suite", "settings.agentBehaviour.notImplemented": "Ainda não implementado.", "settings.autoApprove.setAll": "Definir todas as permissões", "settings.autoApprove.level.allow": "Permitir", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index d8105d6e637..d48e3e48e8e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -928,6 +928,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "Nema konfiguriranih MCP servera. Uredite konfiguracijsku datoteku opencode za dodavanje MCP servera.", "settings.agentBehaviour.workflowsPlaceholder": "Tokovi rada se upravljaju putem datoteka tokova rada.", + "settings.agentBehaviour.workflowsEmpty": + "Nisu pronađeni tokovi rada. Dodajte .md datoteke u .kilocode/workflows/ u vašem projektu ili ~/.kilocode/workflows/ globalno.", + "settings.agentBehaviour.workflows.title": "Tokovi rada", + "settings.agentBehaviour.workflows.description": + "Tokovi rada su .md datoteke u .kilocode/workflows/. Koristite /naziv u chatu za pokretanje.", + "settings.agentBehaviour.workflows.namePlaceholder": "npr. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Još nije implementirano.", "settings.autoApprove.setAll": "Postavi sve dozvole", "settings.autoApprove.level.allow": "Dozvoli", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 46ae352f718..86edcfbe521 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -901,6 +901,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "Ingen MCP-servere konfigureret. Rediger opencode-konfigurationsfilen for at tilføje MCP-servere.", "settings.agentBehaviour.workflowsPlaceholder": "Workflows administreres via workflow-filer i dit arbejdsområde.", + "settings.agentBehaviour.workflowsEmpty": + "Ingen workflows fundet. Tilføj .md-filer til .kilocode/workflows/ i dit projekt eller ~/.kilocode/workflows/ globalt.", + "settings.agentBehaviour.workflows.title": "Workflows", + "settings.agentBehaviour.workflows.description": + "Workflows er .md-filer i .kilocode/workflows/. Brug /navn i chatten for at køre dem.", + "settings.agentBehaviour.workflows.namePlaceholder": "f.eks. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Endnu ikke implementeret.", "settings.autoApprove.setAll": "Indstil alle tilladelser", "settings.autoApprove.level.allow": "Tillad", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 23838bc1430..1b568e21922 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -913,6 +913,12 @@ export const dict = { "Keine MCP-Server konfiguriert. Bearbeiten Sie die opencode-Konfigurationsdatei, um MCP-Server hinzuzufügen.", "settings.agentBehaviour.workflowsPlaceholder": "Workflows werden über Workflow-Dateien in Ihrem Arbeitsbereich verwaltet.", + "settings.agentBehaviour.workflowsEmpty": + "Keine Workflows gefunden. Fügen Sie .md-Dateien zu .kilocode/workflows/ in Ihrem Projekt oder ~/.kilocode/workflows/ global hinzu.", + "settings.agentBehaviour.workflows.title": "Workflows", + "settings.agentBehaviour.workflows.description": + "Workflows sind .md-Dateien in .kilocode/workflows/. Verwenden Sie /name im Chat, um sie auszuführen.", + "settings.agentBehaviour.workflows.namePlaceholder": "z.B. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Noch nicht implementiert.", "settings.autoApprove.setAll": "Alle Berechtigungen festlegen", "settings.autoApprove.level.allow": "Erlauben", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 5629e2a2b7a..20bd7c01987 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -940,6 +940,12 @@ export const dict = { "Paths to additional instruction files that are included in the system prompt", "settings.agentBehaviour.mcpEmpty": "No MCP servers configured. Edit the opencode config file to add MCP servers.", "settings.agentBehaviour.workflowsPlaceholder": "Workflows are managed via workflow files in your workspace.", + "settings.agentBehaviour.workflowsEmpty": + "No workflows found. Add .md files to .kilocode/workflows/ in your project or ~/.kilocode/workflows/ globally.", + "settings.agentBehaviour.workflows.title": "Workflows", + "settings.agentBehaviour.workflows.description": + "Workflows are .md files in .kilocode/workflows/. Use /name in the chat to run them.", + "settings.agentBehaviour.workflows.namePlaceholder": "e.g. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Not yet implemented.", "settings.autoApprove.setAll": "Set all permissions", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index edfc9c9940a..c936b8f38d4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -909,6 +909,12 @@ export const dict = { "No hay servidores MCP configurados. Edite el archivo de configuración de opencode para añadir servidores MCP.", "settings.agentBehaviour.workflowsPlaceholder": "Los flujos de trabajo se gestionan mediante archivos de flujo de trabajo en su espacio de trabajo.", + "settings.agentBehaviour.workflowsEmpty": + "No se encontraron flujos de trabajo. Añada archivos .md en .kilocode/workflows/ en su proyecto o ~/.kilocode/workflows/ globalmente.", + "settings.agentBehaviour.workflows.title": "Flujos de trabajo", + "settings.agentBehaviour.workflows.description": + "Los flujos de trabajo son archivos .md en .kilocode/workflows/. Use /nombre en el chat para ejecutarlos.", + "settings.agentBehaviour.workflows.namePlaceholder": "ej. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Aún no implementado.", "settings.autoApprove.setAll": "Establecer todos los permisos", "settings.autoApprove.level.allow": "Permitir", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index b6d1bce8670..8dd9e5cd43b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -917,6 +917,12 @@ export const dict = { "Aucun serveur MCP configuré. Modifiez le fichier de configuration opencode pour ajouter des serveurs MCP.", "settings.agentBehaviour.workflowsPlaceholder": "Les workflows sont gérés via les fichiers de workflow dans votre espace de travail.", + "settings.agentBehaviour.workflowsEmpty": + "Aucun workflow trouvé. Ajoutez des fichiers .md dans .kilocode/workflows/ de votre projet ou ~/.kilocode/workflows/ globalement.", + "settings.agentBehaviour.workflows.title": "Workflows", + "settings.agentBehaviour.workflows.description": + "Les workflows sont des fichiers .md dans .kilocode/workflows/. Utilisez /nom dans le chat pour les exécuter.", + "settings.agentBehaviour.workflows.namePlaceholder": "ex. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Pas encore implémenté.", "settings.autoApprove.setAll": "Définir toutes les autorisations", "settings.autoApprove.level.allow": "Autoriser", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 6ffbf3c3b38..f3b22981618 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -897,6 +897,12 @@ export const dict = { "MCPサーバーが設定されていません。opencode設定ファイルを編集してMCPサーバーを追加してください。", "settings.agentBehaviour.workflowsPlaceholder": "ワークフローはワークスペース内のワークフローファイルを通じて管理されます。", + "settings.agentBehaviour.workflowsEmpty": + "ワークフローが見つかりません。プロジェクトの .kilocode/workflows/ または ~/.kilocode/workflows/ にグローバルに .md ファイルを追加してください。", + "settings.agentBehaviour.workflows.title": "ワークフロー", + "settings.agentBehaviour.workflows.description": + "ワークフローは .kilocode/workflows/ の .md ファイルです。チャットで /名前 を使用して実行します。", + "settings.agentBehaviour.workflows.namePlaceholder": "例: deploy, test-suite", "settings.agentBehaviour.notImplemented": "まだ実装されていません。", "settings.autoApprove.setAll": "すべての権限を設定", "settings.autoApprove.level.allow": "許可", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 602dc786dc5..2757efbe7c0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -898,6 +898,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "MCP 서버가 구성되지 않았습니다. opencode 구성 파일을 편집하여 MCP 서버를 추가하세요.", "settings.agentBehaviour.workflowsPlaceholder": "워크플로우는 워크스페이스의 워크플로우 파일을 통해 관리됩니다.", + "settings.agentBehaviour.workflowsEmpty": + "워크플로우를 찾을 수 없습니다. 프로젝트의 .kilocode/workflows/ 또는 ~/.kilocode/workflows/에 .md 파일을 추가하세요.", + "settings.agentBehaviour.workflows.title": "워크플로우", + "settings.agentBehaviour.workflows.description": + "워크플로우는 .kilocode/workflows/의 .md 파일입니다. 채팅에서 /이름을 사용하여 실행하세요.", + "settings.agentBehaviour.workflows.namePlaceholder": "예: deploy, test-suite", "settings.agentBehaviour.notImplemented": "아직 구현되지 않았습니다.", "settings.autoApprove.setAll": "모든 권한 설정", "settings.autoApprove.level.allow": "허용", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 28f531ce427..312cf8fece5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -903,6 +903,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "Ingen MCP-servere konfigurert. Rediger opencode-konfigurasjonsfilen for å legge til MCP-servere.", "settings.agentBehaviour.workflowsPlaceholder": "Arbeidsflyter administreres via arbeidsflytfiler i arbeidsområdet.", + "settings.agentBehaviour.workflowsEmpty": + "Ingen arbeidsflyter funnet. Legg til .md-filer i .kilocode/workflows/ i prosjektet ditt eller ~/.kilocode/workflows/ globalt.", + "settings.agentBehaviour.workflows.title": "Arbeidsflyter", + "settings.agentBehaviour.workflows.description": + "Arbeidsflyter er .md-filer i .kilocode/workflows/. Bruk /navn i chatten for å kjøre dem.", + "settings.agentBehaviour.workflows.namePlaceholder": "f.eks. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Ikke implementert ennå.", "settings.autoApprove.setAll": "Sett alle tillatelser", "settings.autoApprove.level.allow": "Tillat", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 08bff07a9a8..3e79304f49f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -904,6 +904,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "Brak skonfigurowanych serwerów MCP. Edytuj plik konfiguracyjny opencode, aby dodać serwery MCP.", "settings.agentBehaviour.workflowsPlaceholder": "Przepływy pracy zarządzane są za pomocą plików przepływów pracy.", + "settings.agentBehaviour.workflowsEmpty": + "Nie znaleziono przepływów pracy. Dodaj pliki .md do .kilocode/workflows/ w projekcie lub ~/.kilocode/workflows/ globalnie.", + "settings.agentBehaviour.workflows.title": "Przepływy pracy", + "settings.agentBehaviour.workflows.description": + "Przepływy pracy to pliki .md w .kilocode/workflows/. Użyj /nazwa w czacie, aby je uruchomić.", + "settings.agentBehaviour.workflows.namePlaceholder": "np. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Jeszcze nie zaimplementowano.", "settings.autoApprove.setAll": "Ustaw wszystkie uprawnienia", "settings.autoApprove.level.allow": "Zezwól", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 97de122273d..1c3d1f9fad9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -906,6 +906,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "MCP-серверы не настроены. Отредактируйте файл конфигурации opencode для добавления MCP-серверов.", "settings.agentBehaviour.workflowsPlaceholder": "Рабочие процессы управляются через файлы рабочих процессов.", + "settings.agentBehaviour.workflowsEmpty": + "Рабочие процессы не найдены. Добавьте .md файлы в .kilocode/workflows/ вашего проекта или ~/.kilocode/workflows/ глобально.", + "settings.agentBehaviour.workflows.title": "Рабочие процессы", + "settings.agentBehaviour.workflows.description": + "Рабочие процессы — это .md файлы в .kilocode/workflows/. Используйте /имя в чате для запуска.", + "settings.agentBehaviour.workflows.namePlaceholder": "напр. deploy, test-suite", "settings.agentBehaviour.notImplemented": "Ещё не реализовано.", "settings.autoApprove.setAll": "Установить все разрешения", "settings.autoApprove.level.allow": "Разрешить", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 3e38eaffff8..15346368db2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -890,6 +890,12 @@ export const dict = { "settings.agentBehaviour.mcpEmpty": "ไม่ได้กำหนดค่าเซิร์ฟเวอร์ MCP แก้ไขไฟล์กำหนดค่า opencode เพื่อเพิ่มเซิร์ฟเวอร์ MCP", "settings.agentBehaviour.workflowsPlaceholder": "เวิร์กโฟลว์จัดการผ่านไฟล์เวิร์กโฟลว์ในพื้นที่ทำงาน", + "settings.agentBehaviour.workflowsEmpty": + "ไม่พบเวิร์กโฟลว์ เพิ่มไฟล์ .md ใน .kilocode/workflows/ ของโปรเจกต์หรือ ~/.kilocode/workflows/ แบบทั่วไป", + "settings.agentBehaviour.workflows.title": "เวิร์กโฟลว์", + "settings.agentBehaviour.workflows.description": + "เวิร์กโฟลว์คือไฟล์ .md ใน .kilocode/workflows/ ใช้ /ชื่อ ในแชทเพื่อเรียกใช้", + "settings.agentBehaviour.workflows.namePlaceholder": "เช่น deploy, test-suite", "settings.agentBehaviour.notImplemented": "ยังไม่ได้ใช้งาน", "settings.autoApprove.setAll": "ตั้งค่าสิทธิ์ทั้งหมด", "settings.autoApprove.level.allow": "อนุญาต", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 39623f50d99..59f929cf482 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -893,6 +893,12 @@ export const dict = { "settings.agentBehaviour.instructionFiles.description": "包含在系统提示词中的附加指令文件路径", "settings.agentBehaviour.mcpEmpty": "未配置 MCP 服务器。编辑 opencode 配置文件以添加 MCP 服务器。", "settings.agentBehaviour.workflowsPlaceholder": "工作流通过工作区中的工作流文件管理。", + "settings.agentBehaviour.workflowsEmpty": + "未找到工作流。请在项目的 .kilocode/workflows/ 或全局的 ~/.kilocode/workflows/ 中添加 .md 文件。", + "settings.agentBehaviour.workflows.title": "工作流", + "settings.agentBehaviour.workflows.description": + "工作流是 .kilocode/workflows/ 中的 .md 文件。在聊天中使用 /名称 来运行它们。", + "settings.agentBehaviour.workflows.namePlaceholder": "例如:deploy, test-suite", "settings.agentBehaviour.notImplemented": "尚未实现。", "settings.autoApprove.setAll": "设置所有权限", "settings.autoApprove.level.allow": "允许", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index d5ef0c9a98d..397177d8aec 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -889,6 +889,12 @@ export const dict = { "settings.agentBehaviour.instructionFiles.description": "包含在系統提示詞中的附加指令檔案路徑", "settings.agentBehaviour.mcpEmpty": "未設定 MCP 伺服器。編輯 opencode 設定檔以新增 MCP 伺服器。", "settings.agentBehaviour.workflowsPlaceholder": "工作流程透過工作區中的工作流程檔案管理。", + "settings.agentBehaviour.workflowsEmpty": + "未找到工作流程。請在專案的 .kilocode/workflows/ 或全域的 ~/.kilocode/workflows/ 中新增 .md 檔案。", + "settings.agentBehaviour.workflows.title": "工作流程", + "settings.agentBehaviour.workflows.description": + "工作流程是 .kilocode/workflows/ 中的 .md 檔案。在聊天中使用 /名稱 來執行它們。", + "settings.agentBehaviour.workflows.namePlaceholder": "例如:deploy, test-suite", "settings.agentBehaviour.notImplemented": "尚未實作。", "settings.autoApprove.setAll": "設定所有權限", "settings.autoApprove.level.allow": "允許", diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index c0485d4ce34..9abe36fa354 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -484,6 +484,73 @@ text-align: center; } +/* ============================================ + Slash Command Dropdown + ============================================ */ + +.slash-command-dropdown { + position: absolute; + bottom: calc(100% + 4px); + left: 0; + right: 0; + z-index: 10; + background: var(--vscode-editorWidget-background, var(--vscode-input-background)); + border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border)); + border-radius: 4px; + overflow: hidden; + max-height: 240px; + overflow-y: auto; +} + +.slash-command-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + cursor: pointer; + font-size: 12px; + color: var(--vscode-foreground); + min-width: 0; +} + +.slash-command-item--active { + background: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} + +.slash-command-name { + flex-shrink: 0; + font-weight: 500; + font-family: var(--vscode-editor-font-family, monospace); +} + +.slash-command-description { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.6; + font-size: 11px; +} + +.slash-command-badge { + flex-shrink: 0; + font-size: 10px; + padding: 1px 5px; + border-radius: 3px; + background: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.slash-command-empty { + padding: 8px 10px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + text-align: center; +} + /* ============================================ Model Selector (uses kilo-ui Popover) ============================================ */ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 2e05d76db73..4fa73bba4cc 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -174,6 +174,15 @@ export interface AgentInfo { color?: string } +// Command/workflow info from CLI backend +export interface CommandInfo { + name: string + description?: string + source?: "command" | "mcp" | "skill" + hints: string[] + workflowScope?: "project" | "global" +} + // Server info export interface ServerInfo { port: number @@ -451,6 +460,7 @@ export interface CloudSessionImportFailedMessage { type: "cloudSessionImportFailed" cloudSessionId: string error: string + sessionID?: string } export interface OpenCloudSessionMessage { @@ -575,6 +585,12 @@ export interface ConfigUpdatedMessage { config: Config } +export interface CommandsLoadedMessage { + type: "commandsLoaded" + commands: CommandInfo[] + error?: string +} + export interface NotificationSettingsLoadedMessage { type: "notificationSettingsLoaded" settings: { @@ -807,6 +823,7 @@ export type ExtensionMessage = | BrowserSettingsLoadedMessage | ConfigLoadedMessage | ConfigUpdatedMessage + | CommandsLoadedMessage | NotificationSettingsLoadedMessage | NotificationsLoadedMessage | AgentManagerSessionMetaMessage @@ -853,6 +870,26 @@ export interface SendMessageRequest { files?: FileAttachment[] } +export interface SendCommandRequest { + type: "sendCommand" + command: string + arguments: string + sessionID?: string + providerID?: string + modelID?: string + agent?: string +} + +export interface ImportAndSendCommandMessage { + type: "importAndSendCommand" + cloudSessionId: string + command: string + arguments: string + providerID?: string + modelID?: string + agent?: string +} + export interface AbortRequest { type: "abort" sessionID: string @@ -933,6 +970,24 @@ export interface OpenFileRequest { column?: number } +export interface OpenWorkflowFileRequest { + type: "openWorkflowFile" + name: string + workflowScope?: "project" | "global" +} + +export interface CreateWorkflowFileRequest { + type: "createWorkflowFile" + name: string + workflowScope?: "project" | "global" +} + +export interface DeleteWorkflowFileRequest { + type: "deleteWorkflowFile" + name: string + workflowScope?: "project" | "global" +} + export interface CancelLoginRequest { type: "cancelLogin" } @@ -1028,6 +1083,10 @@ export interface RequestConfigMessage { type: "requestConfig" } +export interface RequestCommandsMessage { + type: "requestCommands" +} + export interface UpdateConfigMessage { type: "updateConfig" config: Partial @@ -1246,6 +1305,8 @@ export interface RequestVariantsMessage { export type WebviewMessage = | SendMessageRequest + | SendCommandRequest + | ImportAndSendCommandMessage | AbortRequest | PermissionResponseRequest | CreateSessionRequest @@ -1259,6 +1320,9 @@ export type WebviewMessage = | RefreshProfileRequest | OpenExternalRequest | OpenFileRequest + | OpenWorkflowFileRequest + | CreateWorkflowFileRequest + | DeleteWorkflowFileRequest | CancelLoginRequest | SetOrganizationRequest | WebviewReadyRequest @@ -1278,6 +1342,7 @@ export type WebviewMessage = | UpdateSettingRequest | RequestBrowserSettingsMessage | RequestConfigMessage + | RequestCommandsMessage | UpdateConfigMessage | RequestNotificationSettingsMessage | ResetAllSettingsRequest