-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(vscode): add workflow management and slash command support #6260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
955e18f
2de2bb3
a569f1e
8506487
80e358f
5efee6f
9ae78fa
bf68b6d
28ea8dd
73835cf
e066930
3c8488b
839c7b2
b4ae76d
d8afb75
1ca38ac
306d46f
ddeaeb8
d069c98
2053b8f
8a58712
2140c55
06c5f45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||
| import * as path from "path" | ||||||||||||||
| import * as fs from "fs" | ||||||||||||||
| import * as vscode from "vscode" | ||||||||||||||
| import { z } from "zod" | ||||||||||||||
| import { | ||||||||||||||
|
|
@@ -38,6 +39,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 | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -359,6 +362,25 @@ 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 "openWorkflowFile": | ||||||||||||||
| await this.handleOpenWorkflowFile(message.name) | ||||||||||||||
| break | ||||||||||||||
| case "createWorkflowFile": | ||||||||||||||
| await this.handleCreateWorkflowFile(message.name) | ||||||||||||||
| break | ||||||||||||||
| case "deleteWorkflowFile": | ||||||||||||||
| await this.handleDeleteWorkflowFile(message.name) | ||||||||||||||
| break | ||||||||||||||
| case "requestProviders": | ||||||||||||||
| this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e)) | ||||||||||||||
| break | ||||||||||||||
|
|
@@ -377,6 +399,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 | ||||||||||||||
|
|
@@ -594,6 +619,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper | |||||||||||||
| this.fetchAndSendProviders(), | ||||||||||||||
| this.fetchAndSendAgents(), | ||||||||||||||
| this.fetchAndSendConfig(), | ||||||||||||||
| this.fetchAndSendCommands(), | ||||||||||||||
| this.fetchAndSendNotifications(), | ||||||||||||||
| ]) | ||||||||||||||
| this.sendNotificationSettings() | ||||||||||||||
|
|
@@ -976,6 +1002,37 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper | |||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Fetch available commands (workflows, skills, MCP prompts) and send to webview. | ||||||||||||||
| */ | ||||||||||||||
| private async fetchAndSendCommands(): Promise<void> { | ||||||||||||||
| if (!this.httpClient) { | ||||||||||||||
| if (this.cachedCommandsMessage) { | ||||||||||||||
| this.postMessage(this.cachedCommandsMessage) | ||||||||||||||
| } | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| const workspaceDir = this.getWorkspaceDirectory() | ||||||||||||||
| const commands = await this.httpClient.listCommands(workspaceDir) | ||||||||||||||
|
|
||||||||||||||
| const message = { | ||||||||||||||
| type: "commandsLoaded", | ||||||||||||||
| commands: commands.map((cmd) => ({ | ||||||||||||||
| name: cmd.name, | ||||||||||||||
| description: cmd.description, | ||||||||||||||
| source: cmd.source, | ||||||||||||||
| hints: cmd.hints, | ||||||||||||||
| })), | ||||||||||||||
| } | ||||||||||||||
| this.cachedCommandsMessage = message | ||||||||||||||
| this.postMessage(message) | ||||||||||||||
| } catch (error) { | ||||||||||||||
| console.error("[Kilo New] KiloProvider: Failed to fetch commands:", error) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Fetch Kilo news/notifications and send to webview. | ||||||||||||||
| * Uses the cached message pattern so the webview gets data immediately on refresh. | ||||||||||||||
|
|
@@ -1234,6 +1291,163 @@ 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: string, | ||||||||||||||
| sessionID?: string, | ||||||||||||||
| providerID?: string, | ||||||||||||||
| modelID?: string, | ||||||||||||||
| agent?: string, | ||||||||||||||
| ): Promise<void> { | ||||||||||||||
| if (!this.httpClient) { | ||||||||||||||
| this.postMessage({ type: "error", message: "Not connected to CLI backend" }) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| const workspaceDir = this.getWorkspaceDirectory(sessionID || this.currentSession?.id) | ||||||||||||||
|
|
||||||||||||||
| // 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, command, args, workspaceDir, { agent, model }) | ||||||||||||||
|
Githubguy132010 marked this conversation as resolved.
Outdated
|
||||||||||||||
| } catch (error) { | ||||||||||||||
| console.error("[Kilo New] KiloProvider: Failed to execute command:", error) | ||||||||||||||
| this.postMessage({ | ||||||||||||||
| type: "error", | ||||||||||||||
| message: error instanceof Error ? error.message : "Failed to execute command", | ||||||||||||||
| }) | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: The webview's error handler in
This is inconsistent with
Suggested change
|
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Resolve the project-level workflows directory. | ||||||||||||||
| */ | ||||||||||||||
| private getWorkflowsDir(): string { | ||||||||||||||
| const workspaceDir = this.getWorkspaceDirectory() | ||||||||||||||
| return path.join(workspaceDir, ".kilocode", "workflows") | ||||||||||||||
|
Githubguy132010 marked this conversation as resolved.
Outdated
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 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): Promise<void> { | ||||||||||||||
|
Githubguy132010 marked this conversation as resolved.
Outdated
|
||||||||||||||
| const validated = this.validateWorkflowName(name) | ||||||||||||||
| if (!validated) { | ||||||||||||||
| this.postMessage({ type: "error", message: "Invalid workflow name" }) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const filePath = path.join(this.getWorkflowsDir(), `${validated}.md`) | ||||||||||||||
| try { | ||||||||||||||
| const uri = vscode.Uri.file(filePath) | ||||||||||||||
| 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): Promise<void> { | ||||||||||||||
| const validated = this.validateWorkflowName(name) | ||||||||||||||
| if (!validated) { | ||||||||||||||
| this.postMessage({ type: "error", message: "Invalid workflow name" }) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const dir = this.getWorkflowsDir() | ||||||||||||||
| 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): Promise<void> { | ||||||||||||||
| 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 filePath = path.join(this.getWorkflowsDir(), `${validated}.md`) | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| await fs.promises.unlink(filePath) | ||||||||||||||
|
Githubguy132010 marked this conversation as resolved.
Outdated
|
||||||||||||||
| 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. | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.