From 65c969943b924c8eed6a220886c5e1448917509d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Oct 2025 11:09:27 +0530 Subject: [PATCH 1/9] feat(cli): add custom modes support from YAML/JSON files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ability for CLI to load and use custom modes from: - Global: custom_modes.yaml in VS Code global storage - Project: .kilocodemodes in workspace root Changes: - Created customModes loader with platform-aware path resolution - Updated CLI entry point to load and validate custom modes - Extended ExtensionHost to accept and use custom modes - Modified /mode command to display and switch to custom modes - Added customModes to CommandContext for all commands Resolves #3304 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cli/src/cli.ts | 7 + cli/src/commands/core/types.ts | 4 +- cli/src/commands/mode.ts | 45 ++---- cli/src/config/customModes.ts | 178 +++++++++++++++++++++++ cli/src/host/ExtensionHost.ts | 5 +- cli/src/index.ts | 24 ++- cli/src/services/extension.ts | 8 +- cli/src/state/hooks/useCommandContext.ts | 4 + 8 files changed, 234 insertions(+), 41 deletions(-) create mode 100644 cli/src/config/customModes.ts diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 84196deb162..09910c40d33 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -13,12 +13,15 @@ import { requestRouterModelsAtom } from "./state/atoms/actions.js" import { loadHistoryAtom } from "./state/atoms/history.js" import { getTelemetryService, getIdentityManager } from "./services/telemetry/index.js" +import type { ModeConfig } from "./types/messages.js" + export interface CLIOptions { mode?: string workspace?: string ci?: boolean prompt?: string timeout?: number + customModes?: ModeConfig[] } /** @@ -88,6 +91,10 @@ export class CLI { } } + if (this.options.customModes) { + serviceOptions.customModes = this.options.customModes + } + this.service = createExtensionService(serviceOptions) logs.debug("ExtensionService created with identity", "CLI", { hasIdentity: !!identity, diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index 1cabc302dae..be72b52df3a 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -2,7 +2,7 @@ * Command system type definitions */ -import type { RouterModels } from "../../types/messages.js" +import type { RouterModels, ModeConfig } from "../../types/messages.js" import type { ProviderConfig } from "../../config/types.js" import type { ProfileData, BalanceData } from "../../state/atoms/profile.js" @@ -53,6 +53,8 @@ export interface CommandContext { balanceData: BalanceData | null profileLoading: boolean balanceLoading: boolean + // Custom modes context + customModes: ModeConfig[] } export type CommandHandler = (context: CommandContext) => Promise | void diff --git a/cli/src/commands/mode.ts b/cli/src/commands/mode.ts index aa9f03b907b..f5dc238e95e 100644 --- a/cli/src/commands/mode.ts +++ b/cli/src/commands/mode.ts @@ -3,16 +3,7 @@ */ import type { Command, ArgumentValue } from "./core/types.js" -import { DEFAULT_MODES } from "../constants/modes/defaults.js" - -// Convert modes to ArgumentValue format -const MODE_VALUES: ArgumentValue[] = DEFAULT_MODES.map((mode) => ({ - value: mode.slug, - ...(mode.description && { description: mode.description }), -})) - -// Extract mode slugs for validation -const AVAILABLE_MODE_SLUGS = DEFAULT_MODES.map((mode) => mode.slug) +import { DEFAULT_MODES, getAllModes } from "../constants/modes/defaults.js" export const modeCommand: Command = { name: "mode", @@ -27,32 +18,28 @@ export const modeCommand: Command = { name: "mode-name", description: "The mode to switch to", required: true, - values: MODE_VALUES, + // Values will be populated dynamically from context placeholder: "Select a mode", - validate: (value) => { - const isValid = AVAILABLE_MODE_SLUGS.includes(value.toLowerCase()) - return { - valid: isValid, - ...(isValid ? {} : { error: `Invalid mode. Available: ${AVAILABLE_MODE_SLUGS.join(", ")}` }), - } - }, }, ], handler: async (context) => { - const { args, addMessage, setMode } = context + const { args, addMessage, setMode, customModes } = context + + // Get all available modes (default + custom) + const allModes = getAllModes(customModes) + const availableSlugs = allModes.map((mode) => mode.slug) if (args.length === 0 || !args[0]) { // Show current mode and available modes + const modesList = allModes.map((mode) => { + const source = mode.source === "project" ? " (project)" : mode.source === "global" ? " (global)" : "" + return ` - **${mode.name}** (${mode.slug})${source}: ${mode.description || "No description"}` + }) + addMessage({ id: Date.now().toString(), type: "system", - content: [ - "**Available Modes:**", - "", - ...DEFAULT_MODES.map((mode) => ` - **${mode.name}** (${mode.slug}): ${mode.description}`), - "", - "Usage: /mode ", - ].join("\n"), + content: ["**Available Modes:**", "", ...modesList, "", "Usage: /mode "].join("\n"), ts: Date.now(), }) return @@ -60,18 +47,18 @@ export const modeCommand: Command = { const requestedMode = args[0].toLowerCase() - if (!AVAILABLE_MODE_SLUGS.includes(requestedMode)) { + if (!availableSlugs.includes(requestedMode)) { addMessage({ id: Date.now().toString(), type: "error", - content: `Invalid mode "${requestedMode}". Available modes: ${AVAILABLE_MODE_SLUGS.join(", ")}`, + content: `Invalid mode "${requestedMode}". Available modes: ${availableSlugs.join(", ")}`, ts: Date.now(), }) return } // Find the mode to get its display name - const mode = DEFAULT_MODES.find((m) => m.slug === requestedMode) + const mode = allModes.find((m) => m.slug === requestedMode) const modeName = mode?.name || requestedMode setMode(requestedMode) diff --git a/cli/src/config/customModes.ts b/cli/src/config/customModes.ts new file mode 100644 index 00000000000..bfd7b2488ce --- /dev/null +++ b/cli/src/config/customModes.ts @@ -0,0 +1,178 @@ +/** + * Custom modes loader + * Loads custom modes from global and project-specific configuration files + */ + +import { readFile } from "fs/promises" +import { existsSync } from "fs" +import { join } from "path" +import { homedir } from "os" +import { parse } from "yaml" +import type { ModeConfig } from "../types/messages.js" + +/** + * Get the global custom modes file path + * @returns Path to global custom_modes.yaml + */ +function getGlobalModesPath(): string { + // VS Code global storage path varies by platform + const homeDir = homedir() + + // Try to construct the path to VS Code global storage + // This matches the path used by the VS Code extension + if (process.platform === "darwin") { + // macOS + return join( + homeDir, + "Library", + "Application Support", + "Code", + "User", + "globalStorage", + "kilocode.kilo-code", + "settings", + "custom_modes.yaml", + ) + } else if (process.platform === "win32") { + // Windows + return join( + homeDir, + "AppData", + "Roaming", + "Code", + "User", + "globalStorage", + "kilocode.kilo-code", + "settings", + "custom_modes.yaml", + ) + } else { + // Linux + return join( + homeDir, + ".config", + "Code", + "User", + "globalStorage", + "kilocode.kilo-code", + "settings", + "custom_modes.yaml", + ) + } +} + +/** + * Get the project custom modes file path + * @param workspace - Workspace directory path + * @returns Path to .kilocodemodes + */ +function getProjectModesPath(workspace: string): string { + return join(workspace, ".kilocodemodes") +} + +/** + * Parse custom modes from YAML content + * @param content - YAML file content + * @param source - Source of the modes ('global' or 'project') + * @returns Array of mode configurations + */ +function parseCustomModes(content: string, source: "global" | "project"): ModeConfig[] { + try { + const parsed = parse(content) + + if (!parsed || typeof parsed !== "object") { + return [] + } + + // Handle both YAML format (customModes array) and JSON format + const modes = parsed.customModes || [] + + if (!Array.isArray(modes)) { + return [] + } + + // Validate and normalize mode configs + return modes + .filter((mode: any) => { + // Must have at least slug and name + return mode && typeof mode === "object" && mode.slug && mode.name + }) + .map((mode: any) => ({ + slug: mode.slug, + name: mode.name, + description: mode.description, + systemPrompt: mode.roleDefinition || mode.systemPrompt, + rules: mode.customInstructions ? [mode.customInstructions] : mode.rules || [], + source: mode.source || source, + })) + } catch (error) { + // Silent fail - return empty array if parsing fails + return [] + } +} + +/** + * Load custom modes from global configuration + * @returns Array of global custom modes + */ +async function loadGlobalCustomModes(): Promise { + const globalPath = getGlobalModesPath() + + if (!existsSync(globalPath)) { + return [] + } + + try { + const content = await readFile(globalPath, "utf-8") + return parseCustomModes(content, "global") + } catch (error) { + // Silent fail - return empty array if reading fails + return [] + } +} + +/** + * Load custom modes from project configuration + * @param workspace - Workspace directory path + * @returns Array of project custom modes + */ +async function loadProjectCustomModes(workspace: string): Promise { + const projectPath = getProjectModesPath(workspace) + + if (!existsSync(projectPath)) { + return [] + } + + try { + const content = await readFile(projectPath, "utf-8") + return parseCustomModes(content, "project") + } catch (error) { + // Silent fail - return empty array if reading fails + return [] + } +} + +/** + * Load all custom modes (global + project) + * Project modes override global modes with the same slug + * @param workspace - Workspace directory path + * @returns Array of all custom mode configurations + */ +export async function loadCustomModes(workspace: string): Promise { + const [globalModes, projectModes] = await Promise.all([loadGlobalCustomModes(), loadProjectCustomModes(workspace)]) + + // Merge modes, with project modes taking precedence over global modes + const modesMap = new Map() + + // Add global modes first + for (const mode of globalModes) { + modesMap.set(mode.slug, mode) + } + + // Override with project modes + for (const mode of projectModes) { + modesMap.set(mode.slug, mode) + } + + return Array.from(modesMap.values()) +} diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index 37e672078d2..adda8d71f0d 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events" import { createVSCodeAPIMock, type IdentityInfo } from "./VSCode.js" import { logs } from "../services/logs.js" -import type { ExtensionMessage, WebviewMessage, ExtensionState } from "../types/messages.js" +import type { ExtensionMessage, WebviewMessage, ExtensionState, ModeConfig } from "../types/messages.js" import { getTelemetryService } from "../services/telemetry/index.js" export interface ExtensionHostOptions { @@ -9,6 +9,7 @@ export interface ExtensionHostOptions { extensionBundlePath: string // Direct path to extension.js extensionRootPath: string // Root path for extension assets identity?: IdentityInfo // Identity information for VSCode environment + customModes?: ModeConfig[] // Custom modes configuration } export interface ExtensionAPI { @@ -674,7 +675,7 @@ export class ExtensionHost extends EventEmitter { }, chatMessages: [], mode: "code", - customModes: [], + customModes: this.options.customModes || [], taskHistoryFullLength: 0, taskHistoryVersion: 0, renderContext: "cli", diff --git a/cli/src/index.ts b/cli/src/index.ts index dffe6c590bd..206af9f1c7d 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -7,17 +7,19 @@ loadEnvFile() import { Command } from "commander" import { existsSync } from "fs" import { CLI } from "./cli.js" -import { DEFAULT_MODES } from "./constants/modes/defaults.js" +import { DEFAULT_MODES, getAllModes } from "./constants/modes/defaults.js" import { getTelemetryService } from "./services/telemetry/index.js" import { Package } from "./constants/package.js" import openConfigFile from "./config/openConfig.js" import authWizard from "./utils/authWizard.js" import { configExists } from "./config/persistence.js" +import { loadCustomModes } from "./config/customModes.js" const program = new Command() let cli: CLI | null = null -// Get list of valid mode slugs +// Get list of valid mode slugs from default modes +// Custom modes will be loaded and validated per workspace const validModes = DEFAULT_MODES.map((mode) => mode.slug) program @@ -30,18 +32,23 @@ program .option("-t, --timeout ", "Timeout in seconds for autonomous mode (requires --auto)", parseInt) .argument("[prompt]", "The prompt or command to execute") .action(async (prompt, options) => { - // Validate mode if provided - if (options.mode && !validModes.includes(options.mode)) { - console.error(`Error: Invalid mode "${options.mode}". Valid modes are: ${validModes.join(", ")}`) - process.exit(1) - } - // Validate workspace path exists if (!existsSync(options.workspace)) { console.error(`Error: Workspace path does not exist: ${options.workspace}`) process.exit(1) } + // Load custom modes from workspace + const customModes = await loadCustomModes(options.workspace) + const allModes = getAllModes(customModes) + const allValidModes = allModes.map((mode) => mode.slug) + + // Validate mode if provided + if (options.mode && !allValidModes.includes(options.mode)) { + console.error(`Error: Invalid mode "${options.mode}". Valid modes are: ${allValidModes.join(", ")}`) + process.exit(1) + } + // Validate that piped stdin requires autonomous mode if (!process.stdin.isTTY && !options.auto) { console.error("Error: Piped input requires --auto flag to be enabled") @@ -94,6 +101,7 @@ program ci: options.auto, prompt: finalPrompt, timeout: options.timeout, + customModes: customModes, }) await cli.start() await cli.dispose() diff --git a/cli/src/services/extension.ts b/cli/src/services/extension.ts index 8457462dde7..61be2393def 100644 --- a/cli/src/services/extension.ts +++ b/cli/src/services/extension.ts @@ -3,7 +3,7 @@ import { createExtensionHost, ExtensionHost, ExtensionAPI, type ExtensionHostOpt import { createMessageBridge, MessageBridge } from "../communication/ipc.js" import { logs } from "./logs.js" import { resolveExtensionPaths } from "../utils/extension-paths.js" -import type { ExtensionMessage, WebviewMessage, ExtensionState } from "../types/messages.js" +import type { ExtensionMessage, WebviewMessage, ExtensionState, ModeConfig } from "../types/messages.js" import type { IdentityInfo } from "../host/VSCode.js" /** @@ -14,6 +14,8 @@ export interface ExtensionServiceOptions { workspace?: string /** Initial mode to start with */ mode?: string + /** Custom modes configuration */ + customModes?: ModeConfig[] /** Custom extension bundle path (for testing) */ extensionBundlePath?: string /** Custom extension root path (for testing) */ @@ -88,6 +90,7 @@ export class ExtensionService extends EventEmitter { extensionBundlePath: options.extensionBundlePath || extensionPaths.extensionBundlePath, extensionRootPath: options.extensionRootPath || extensionPaths.extensionRootPath, ...(options.identity && { identity: options.identity }), + ...(options.customModes && { customModes: options.customModes }), } // Create extension host @@ -99,6 +102,9 @@ export class ExtensionService extends EventEmitter { if (this.options.identity) { hostOptions.identity = this.options.identity } + if (this.options.customModes) { + hostOptions.customModes = this.options.customModes + } this.extensionHost = createExtensionHost(hostOptions) // Create message bridge diff --git a/cli/src/state/hooks/useCommandContext.ts b/cli/src/state/hooks/useCommandContext.ts index 333590d55f4..21c5eacf2e3 100644 --- a/cli/src/state/hooks/useCommandContext.ts +++ b/cli/src/state/hooks/useCommandContext.ts @@ -67,6 +67,7 @@ export function useCommandContext(): UseCommandContextReturn { const currentProvider = useAtomValue(providerAtom) const extensionState = useAtomValue(extensionStateAtom) const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || "" + const customModes = extensionState?.customModes || [] // Get profile state const profileData = useAtomValue(profileDataAtom) @@ -131,6 +132,8 @@ export function useCommandContext(): UseCommandContextReturn { balanceData, profileLoading, balanceLoading, + // Custom modes context + customModes, } }, [ @@ -150,6 +153,7 @@ export function useCommandContext(): UseCommandContextReturn { balanceData, profileLoading, balanceLoading, + customModes, ], ) From e9f1ee85a21a4373696fda3da0dc0d62e1ce84e3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Oct 2025 11:12:42 +0530 Subject: [PATCH 2/9] fix(cli): resolve TypeScript type error for customModes optional property --- cli/src/services/extension.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/services/extension.ts b/cli/src/services/extension.ts index 61be2393def..679c35379a4 100644 --- a/cli/src/services/extension.ts +++ b/cli/src/services/extension.ts @@ -73,7 +73,10 @@ export interface ExtensionServiceEvents { export class ExtensionService extends EventEmitter { private extensionHost: ExtensionHost private messageBridge: MessageBridge - private options: Required> & { identity?: IdentityInfo } + private options: Required> & { + identity?: IdentityInfo + customModes?: ModeConfig[] + } private isInitialized = false private isDisposed = false From b945efab78808f337fef2358d3e48c8f643eb7b9 Mon Sep 17 00:00:00 2001 From: Benson K B Date: Fri, 7 Nov 2025 18:58:01 +0530 Subject: [PATCH 3/9] fix: remove unnecessary semicolons from interface properties --- cli/src/commands/core/types.ts | 234 ++++++++++++++++----------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index 76c68c9d2b3..6d437c1538e 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -8,78 +8,78 @@ import type { ProfileData, BalanceData } from "../../state/atoms/profile.js"; import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js"; export interface Command { - name: string; - aliases: string[]; - description: string; - usage: string; - examples: string[]; - category: "chat" | "settings" | "navigation" | "system"; - handler: CommandHandler; - options?: CommandOption[]; - arguments?: ArgumentDefinition[]; + name: string + aliases: string[] + description: string + usage: string + examples: string[] + category: "chat" | "settings" | "navigation" | "system" + handler: CommandHandler + options?: CommandOption[] + arguments?: ArgumentDefinition[] priority?: number; // 1-10 scale, default 5. Higher = appears first in suggestions } export interface CommandOption { - name: string; - alias?: string; - description: string; - required?: boolean; - type: "string" | "number" | "boolean"; - default?: any; + name: string + alias?: string + description: string + required?: boolean + type: "string" | "number" | "boolean" + default?: any } export interface CommandContext { - input: string; - args: string[]; - options: Record; - config: CLIConfig; - sendMessage: (message: any) => Promise; - addMessage: (message: any) => void; - clearMessages: () => void; - replaceMessages: (messages: any[]) => void; - setMessageCutoffTimestamp: (timestamp: number) => void; - clearTask: () => Promise; - setMode: (mode: string) => void; - setTheme: (theme: string) => Promise; - exit: () => void; - setCommittingParallelMode: (isCommitting: boolean) => void; - isParallelMode: boolean; + input: string + args: string[] + options: Record + config: CLIConfig + sendMessage: (message: any) => Promise + addMessage: (message: any) => void + clearMessages: () => void + replaceMessages: (messages: any[]) => void + setMessageCutoffTimestamp: (timestamp: number) => void + clearTask: () => Promise + setMode: (mode: string) => void + setTheme: (theme: string) => Promise + exit: () => void + setCommittingParallelMode: (isCommitting: boolean) => void + isParallelMode: boolean // Model-related context - routerModels: RouterModels | null; - currentProvider: ProviderConfig | null; - kilocodeDefaultModel: string; - updateProviderModel: (modelId: string) => Promise; - refreshRouterModels: () => Promise; + routerModels: RouterModels | null + currentProvider: ProviderConfig | null + kilocodeDefaultModel: string + updateProviderModel: (modelId: string) => Promise + refreshRouterModels: () => Promise // Provider update function for teams command - updateProvider: (providerId: string, updates: Partial) => Promise; + updateProvider: (providerId: string, updates: Partial) => Promise // Profile data context - profileData: ProfileData | null; - balanceData: BalanceData | null; - profileLoading: boolean; - balanceLoading: boolean; + profileData: ProfileData | null + balanceData: BalanceData | null + profileLoading: boolean + balanceLoading: boolean // Custom modes context - customModes: ModeConfig[]; + customModes: ModeConfig[] // Task history context - taskHistoryData: TaskHistoryData | null; - taskHistoryFilters: TaskHistoryFilters; - taskHistoryLoading: boolean; - taskHistoryError: string | null; - fetchTaskHistory: () => Promise; - updateTaskHistoryFilters: (filters: Partial) => Promise; - changeTaskHistoryPage: (pageIndex: number) => Promise; - nextTaskHistoryPage: () => Promise; - previousTaskHistoryPage: () => Promise; - sendWebviewMessage: (message: any) => Promise; - refreshTerminal: () => Promise; + taskHistoryData: TaskHistoryData | null + taskHistoryFilters: TaskHistoryFilters + taskHistoryLoading: boolean + taskHistoryError: string | null + fetchTaskHistory: () => Promise + updateTaskHistoryFilters: (filters: Partial) => Promise + changeTaskHistoryPage: (pageIndex: number) => Promise + nextTaskHistoryPage: () => Promise + previousTaskHistoryPage: () => Promise + sendWebviewMessage: (message: any) => Promise + refreshTerminal: () => Promise } export type CommandHandler = (context: CommandContext) => Promise | void; export interface ParsedCommand { - command: string; - args: string[]; - options: Record; + command: string + args: string[] + options: Record } // Argument autocompletion types @@ -88,13 +88,13 @@ export interface ParsedCommand { * Argument suggestion with metadata */ export interface ArgumentSuggestion { - value: string; - title?: string; - description?: string; - matchScore: number; - highlightedValue: string; - loading?: boolean; - error?: string; + value: string + title?: string + description?: string + matchScore: number + highlightedValue: string + loading?: boolean + error?: string } /** @@ -102,38 +102,38 @@ export interface ArgumentSuggestion { */ export interface ArgumentProviderContext { // Basic info - commandName: string; - argumentIndex: number; - argumentName: string; + commandName: string + argumentIndex: number + argumentName: string // Current state - currentArgs: string[]; - currentOptions: Record; - partialInput: string; + currentArgs: string[] + currentOptions: Record + partialInput: string // Access to previous arguments by name - getArgument: (name: string) => string | undefined; + getArgument: (name: string) => string | undefined // Access to all parsed values parsedValues: { - args: Record; - options: Record; + args: Record + options: Record }; // Metadata about the command - command: Command; + command: Command // CommandContext properties for providers that need them commandContext?: { - config: CLIConfig; - routerModels: RouterModels | null; - currentProvider: ProviderConfig | null; - kilocodeDefaultModel: string; - profileData: ProfileData | null; - profileLoading: boolean; - updateProviderModel: (modelId: string) => Promise; - refreshRouterModels: () => Promise; - taskHistoryData: TaskHistoryData | null; + config: CLIConfig + routerModels: RouterModels | null + currentProvider: ProviderConfig | null + kilocodeDefaultModel: string + profileData: ProfileData | null + profileLoading: boolean + updateProviderModel: (modelId: string) => Promise + refreshRouterModels: () => Promise + taskHistoryData: TaskHistoryData | null }; } @@ -148,97 +148,97 @@ export type ArgumentProvider = ( * Validation result */ export interface ValidationResult { - valid: boolean; - error?: string; - warning?: string; + valid: boolean + error?: string + warning?: string } /** * Argument dependency */ export interface ArgumentDependency { - argumentName: string; - values?: string[]; - condition?: (value: string, context: ArgumentProviderContext) => boolean; + argumentName: string + values?: string[] + condition?: (value: string, context: ArgumentProviderContext) => boolean } /** * Conditional provider */ export interface ConditionalProvider { - condition: (context: ArgumentProviderContext) => boolean; - provider: ArgumentProvider; + condition: (context: ArgumentProviderContext) => boolean + provider: ArgumentProvider } /** * Cache configuration for providers */ export interface ProviderCacheConfig { - enabled: boolean; - ttl?: number; - keyGenerator?: (context: ArgumentProviderContext) => string; + enabled: boolean + ttl?: number + keyGenerator?: (context: ArgumentProviderContext) => string } /** * Argument value with metadata */ export interface ArgumentValue { - value: string; - description?: string; + value: string + description?: string } /** * Argument definition with provider support */ export interface ArgumentDefinition { - name: string; - description: string; - required?: boolean; + name: string + description: string + required?: boolean // Provider options - provider?: ArgumentProvider; - values?: ArgumentValue[]; - conditionalProviders?: ConditionalProvider[]; - defaultProvider?: ArgumentProvider; + provider?: ArgumentProvider + values?: ArgumentValue[] + conditionalProviders?: ConditionalProvider[] + defaultProvider?: ArgumentProvider // Dependencies - dependsOn?: ArgumentDependency[]; + dependsOn?: ArgumentDependency[] // Validation - validate?: (value: string, context: ArgumentProviderContext) => Promise | ValidationResult; + validate?: (value: string, context: ArgumentProviderContext) => Promise | ValidationResult // Transform - transform?: (value: string) => string; + transform?: (value: string) => string // UI - placeholder?: string; + placeholder?: string // Caching - cache?: ProviderCacheConfig; + cache?: ProviderCacheConfig } /** * Input state for autocomplete */ export interface InputState { - type: "command" | "argument" | "option" | "none"; - commandName?: string; - command?: Command; + type: "command" | "argument" | "option" | "none" + commandName?: string + command?: Command currentArgument?: { - definition: ArgumentDefinition; - index: number; - partialValue: string; + definition: ArgumentDefinition + index: number + partialValue: string }; validation?: { - valid: boolean; - errors: string[]; - warnings: string[]; + valid: boolean + errors: string[] + warnings: string[] }; dependencies?: { - satisfied: boolean; - missing: string[]; + satisfied: boolean + missing: string[] }; } From f6c7b423864c8f03486bb6516726942f8d43967c Mon Sep 17 00:00:00 2001 From: Benson K B Date: Fri, 7 Nov 2025 19:45:55 +0530 Subject: [PATCH 4/9] feat: enhance mode configuration with roleDefinition, groups, and icons - Add roleDefinition, whenToUse, customInstructions, groups, and iconName properties to ModeConfig - Add organization source type to ModeConfig - Update DEFAULT_MODES with comprehensive mode definitions including: - Detailed role definitions for each mode (architect, code, ask, debug, orchestrator) - Icon names for UI integration - Permission groups for feature access control - Custom instructions for specialized mode behavior - When to use guidance for users This enables richer mode configuration and better user guidance on mode selection. --- cli/src/constants/modes/defaults.ts | 46 ++++++++++++++++++++++++++--- cli/src/types/messages.ts | 7 ++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/cli/src/constants/modes/defaults.ts b/cli/src/constants/modes/defaults.ts index dd706bbb0d5..4e3b8b2cf44 100644 --- a/cli/src/constants/modes/defaults.ts +++ b/cli/src/constants/modes/defaults.ts @@ -8,31 +8,69 @@ export const DEFAULT_MODES: ModeConfig[] = [ { slug: "architect", name: "Architect", - description: "Plan and design system architecture", + iconName: "codicon-type-hierarchy-sub", + roleDefinition: + "You are Kilo Code, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", + whenToUse: + "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", + description: "Plan and design before implementation", + groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], + customInstructions: + "1. Do some information gathering (using provided tools) to get more context about the task.\\n\\n2. You should also ask the user clarifying questions to get a better understanding of the task.\\n\\n3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:\\n - Specific and actionable\\n - Listed in logical execution order\\n - Focused on a single, well-defined outcome\\n - Clear enough that another mode could execute it independently\\n\\n **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.\\n\\n4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.\\n\\n5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.\\n\\n6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes (\\\"\\\") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.\\n\\n7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.\\n\\n**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**", source: "global", }, { slug: "code", name: "Code", + iconName: "codicon-code", + roleDefinition: + "You are Kilo Code, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", + whenToUse: + "Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.", description: "Write, modify, and refactor code", + groups: ["read", "edit", "browser", "command", "mcp"], source: "global", }, { slug: "ask", name: "Ask", - description: "Get explanations and answers", + iconName: "codicon-question", + roleDefinition: + "You are Kilo Code, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.", + whenToUse: + "Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.", + description: "Get answers and explanations", + groups: ["read", "browser", "mcp"], + customInstructions: + "You can analyze code, explain concepts, and access external resources. Always answer the user's questions thoroughly, and do not switch to implementing code unless explicitly requested by the user. Include Mermaid diagrams when they clarify your response.", source: "global", }, { slug: "debug", name: "Debug", - description: "Troubleshoot and fix issues", + iconName: "codicon-bug", + roleDefinition: + "You are Kilo Code, an expert software debugger specializing in systematic problem diagnosis and resolution.", + whenToUse: + "Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.", + description: "Diagnose and fix software issues", + groups: ["read", "edit", "browser", "command", "mcp"], + customInstructions: + "Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.", source: "global", }, { slug: "orchestrator", name: "Orchestrator", - description: "Coordinate complex multi-step projects", + iconName: "codicon-run-all", + roleDefinition: + "You are Kilo Code, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.", + whenToUse: + "Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.", + description: "Coordinate tasks across multiple modes", + groups: [], + customInstructions: + "Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:\\n\\n1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.\\n\\n2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. These instructions must include:\\n * All necessary context from the parent task or previous subtasks required to complete the work.\\n * A clearly defined scope, specifying exactly what the subtask should accomplish.\\n * An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.\\n * An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.\\n * A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.\\n\\n3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.\\n\\n4. Help the user understand how the different subtasks fit together in the overall workflow. Provide clear reasoning about why you're delegating specific tasks to specific modes.\\n\\n5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.\\n\\n6. Ask clarifying questions when necessary to better understand how to break down complex tasks effectively.\\n\\n7. Suggest improvements to the workflow based on the results of completed subtasks.\\n\\nUse subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), consider creating new subtasks or switching to a different mode altogether.", source: "global", }, ] diff --git a/cli/src/types/messages.ts b/cli/src/types/messages.ts index d4acbd296ac..5897cd3e058 100644 --- a/cli/src/types/messages.ts +++ b/cli/src/types/messages.ts @@ -397,6 +397,11 @@ export interface ModeConfig { name: string description?: string systemPrompt?: string + roleDefinition?: string + whenToUse?: string + customInstructions?: string rules?: string[] - source?: "global" | "project" + groups?: (string | [string, { fileRegex?: string; description?: string }])[] + iconName?: string + source?: "global" | "project" | "organization" } From 851bafcd5f7313e923d2cfdb12c3958759a7a2f5 Mon Sep 17 00:00:00 2001 From: Benson K B Date: Fri, 7 Nov 2025 19:48:34 +0530 Subject: [PATCH 5/9] fix: improve type safety for custom modes - Change customModes in ExtensionState from any[] to ModeConfig[] - Change customModesAtom from any[] to ModeConfig[] - Import ModeConfig in extension.ts atoms file This ensures proper type checking throughout the custom modes system. --- cli/src/state/atoms/extension.ts | 3 ++- cli/src/types/messages.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/src/state/atoms/extension.ts b/cli/src/state/atoms/extension.ts index 5b4de27ca7e..61b2e504e35 100644 --- a/cli/src/state/atoms/extension.ts +++ b/cli/src/state/atoms/extension.ts @@ -11,6 +11,7 @@ import type { RouterModels, ProviderSettings, McpServer, + ModeConfig, } from "../../types/messages.js" /** @@ -65,7 +66,7 @@ export const extensionModeAtom = atom("code") /** * Atom to hold custom modes */ -export const customModesAtom = atom([]) +export const customModesAtom = atom([]) /** * Atom to hold MCP servers configuration diff --git a/cli/src/types/messages.ts b/cli/src/types/messages.ts index 5897cd3e058..940e477c67a 100644 --- a/cli/src/types/messages.ts +++ b/cli/src/types/messages.ts @@ -378,7 +378,7 @@ export interface ExtensionState { currentTaskItem?: HistoryItem currentTaskTodos?: TodoItem[] mode: string - customModes: any[] + customModes: ModeConfig[] taskHistoryFullLength: number taskHistoryVersion: number mcpServers?: McpServer[] From ee8af1f78b3a34d810539b46f2d1a5ad3702a48d Mon Sep 17 00:00:00 2001 From: Benson K B Date: Fri, 7 Nov 2025 19:53:00 +0530 Subject: [PATCH 6/9] test: add comprehensive test suite for /mode command - Add mode.test.ts with 332 lines of comprehensive test coverage - Test command metadata (name, aliases, description, usage, examples) - Test handler behavior with no arguments (list available modes) - Test handler behavior with valid and invalid mode slugs - Test case-insensitive mode switching - Test source label display (global, project, organization) - Test custom modes integration - Test message structure and timestamps - Coverage includes default modes and custom modes scenarios - Includes tests for error handling and user feedback --- cli/src/commands/__tests__/mode.test.ts | 332 ++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 cli/src/commands/__tests__/mode.test.ts diff --git a/cli/src/commands/__tests__/mode.test.ts b/cli/src/commands/__tests__/mode.test.ts new file mode 100644 index 00000000000..865b25872a5 --- /dev/null +++ b/cli/src/commands/__tests__/mode.test.ts @@ -0,0 +1,332 @@ +/** + * Tests for the /mode command + */ + +import { describe, it, expect, beforeEach, vi } from "vitest" +import { modeCommand } from "../mode.js" +import type { CommandContext } from "../core/types.js" +import type { ModeConfig } from "../../types/messages.js" + +describe("modeCommand", () => { + let mockContext: CommandContext + let mockAddMessage: ReturnType + let mockSetMode: ReturnType + + beforeEach(() => { + mockAddMessage = vi.fn() + mockSetMode = vi.fn() + + mockContext = { + input: "/mode", + args: [], + options: {}, + config: {} as any, + sendMessage: vi.fn().mockResolvedValue(undefined), + addMessage: mockAddMessage, + clearMessages: vi.fn(), + replaceMessages: vi.fn(), + setMessageCutoffTimestamp: vi.fn(), + clearTask: vi.fn().mockResolvedValue(undefined), + setMode: mockSetMode, + setTheme: vi.fn().mockResolvedValue(undefined), + exit: vi.fn(), + setCommittingParallelMode: vi.fn(), + isParallelMode: false, + routerModels: null, + currentProvider: null, + kilocodeDefaultModel: "", + updateProviderModel: vi.fn().mockResolvedValue(undefined), + refreshRouterModels: vi.fn().mockResolvedValue(undefined), + updateProvider: vi.fn().mockResolvedValue(undefined), + profileData: null, + balanceData: null, + profileLoading: false, + balanceLoading: false, + customModes: [], + taskHistoryData: null, + taskHistoryFilters: { + workspace: "current", + sort: "newest", + favoritesOnly: false, + }, + taskHistoryLoading: false, + taskHistoryError: null, + fetchTaskHistory: vi.fn().mockResolvedValue(undefined), + updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), + changeTaskHistoryPage: vi.fn().mockResolvedValue(null), + nextTaskHistoryPage: vi.fn().mockResolvedValue(null), + previousTaskHistoryPage: vi.fn().mockResolvedValue(null), + sendWebviewMessage: vi.fn().mockResolvedValue(undefined), + refreshTerminal: vi.fn().mockResolvedValue(undefined), + } + }) + + describe("command metadata", () => { + it("should have correct name", () => { + expect(modeCommand.name).toBe("mode") + }) + + it("should have correct aliases", () => { + expect(modeCommand.aliases).toEqual(["m"]) + }) + + it("should have correct description", () => { + expect(modeCommand.description).toBe("Switch to a different mode") + }) + + it("should have correct usage", () => { + expect(modeCommand.usage).toBe("/mode ") + }) + + it("should have correct category", () => { + expect(modeCommand.category).toBe("settings") + }) + + it("should have examples", () => { + expect(modeCommand.examples).toEqual(["/mode code", "/mode architect", "/mode debug"]) + }) + + it("should have correct priority", () => { + expect(modeCommand.priority).toBe(9) + }) + + it("should have arguments defined", () => { + expect(modeCommand.arguments).toBeDefined() + expect(modeCommand.arguments?.length).toBe(1) + expect(modeCommand.arguments?.[0].name).toBe("mode-name") + expect(modeCommand.arguments?.[0].required).toBe(true) + }) + }) + + describe("handler - no arguments", () => { + it("should list available default modes when no arguments provided", async () => { + mockContext.args = [] + + await modeCommand.handler(mockContext) + + expect(mockAddMessage).toHaveBeenCalledTimes(1) + const message = mockAddMessage.mock.calls[0][0] + expect(message.type).toBe("system") + expect(message.content).toContain("**Available Modes:**") + expect(message.content).toContain("architect") + expect(message.content).toContain("code") + expect(message.content).toContain("ask") + expect(message.content).toContain("debug") + expect(message.content).toContain("orchestrator") + }) + + it("should show mode descriptions", async () => { + mockContext.args = [] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.content).toContain("(architect)") + expect(message.content).toContain("Plan and design before implementation") + expect(message.content).toContain("(code)") + expect(message.content).toContain("Write, modify, and refactor code") + }) + + it("should show source labels for global modes", async () => { + mockContext.args = [] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.content).toContain("(global)") + }) + + it("should not call setMode when no arguments", async () => { + mockContext.args = [] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).not.toHaveBeenCalled() + }) + }) + + describe("handler - with arguments", () => { + it("should switch to valid mode", async () => { + mockContext.args = ["code"] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).toHaveBeenCalledWith("code") + }) + + it("should show success message when switching mode", async () => { + mockContext.args = ["architect"] + + await modeCommand.handler(mockContext) + + expect(mockAddMessage).toHaveBeenCalledTimes(1) + const message = mockAddMessage.mock.calls[0][0] + expect(message.type).toBe("system") + expect(message.content).toContain("Switched to **Architect** mode") + }) + + it("should be case-insensitive", async () => { + mockContext.args = ["CODE"] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).toHaveBeenCalledWith("code") + }) + + it("should show error for invalid mode", async () => { + mockContext.args = ["invalid-mode"] + + await modeCommand.handler(mockContext) + + expect(mockAddMessage).toHaveBeenCalledTimes(1) + const message = mockAddMessage.mock.calls[0][0] + expect(message.type).toBe("error") + expect(message.content).toContain('Invalid mode "invalid-mode"') + expect(message.content).toContain("Available modes:") + }) + + it("should not call setMode for invalid mode", async () => { + mockContext.args = ["invalid-mode"] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).not.toHaveBeenCalled() + }) + + it("should work with all default modes", async () => { + const modes = ["architect", "code", "ask", "debug", "orchestrator"] + + for (const mode of modes) { + mockAddMessage.mockClear() + mockSetMode.mockClear() + mockContext.args = [mode] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).toHaveBeenCalledWith(mode) + } + }) + }) + + describe("handler - custom modes", () => { + it("should include custom modes in available list", async () => { + const customMode: ModeConfig = { + slug: "custom", + name: "Custom Mode", + description: "A custom mode", + source: "project", + } + mockContext.customModes = [customMode] + mockContext.args = [] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.content).toContain("custom") + expect(message.content).toContain("Custom Mode") + expect(message.content).toContain("(project)") + }) + + it("should switch to custom mode", async () => { + const customMode: ModeConfig = { + slug: "custom", + name: "Custom Mode", + description: "A custom mode", + source: "project", + } + mockContext.customModes = [customMode] + mockContext.args = ["custom"] + + await modeCommand.handler(mockContext) + + expect(mockSetMode).toHaveBeenCalledWith("custom") + }) + + it("should show custom mode in success message", async () => { + const customMode: ModeConfig = { + slug: "custom", + name: "Custom Mode", + description: "A custom mode", + source: "project", + } + mockContext.customModes = [customMode] + mockContext.args = ["custom"] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.content).toContain("Switched to **Custom Mode** mode") + }) + + it("should show organization source label", async () => { + const orgMode: ModeConfig = { + slug: "org-mode", + name: "Org Mode", + description: "An org mode", + source: "organization", + } + mockContext.customModes = [orgMode] + mockContext.args = [] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.content).toContain("org-mode") + }) + + it("should mix default and custom modes", async () => { + const customMode: ModeConfig = { + slug: "custom", + name: "Custom Mode", + description: "A custom mode", + source: "project", + } + mockContext.customModes = [customMode] + mockContext.args = [] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + const content = message.content + + // Should have all default modes + expect(content).toContain("architect") + expect(content).toContain("code") + expect(content).toContain("ask") + expect(content).toContain("debug") + expect(content).toContain("orchestrator") + + // Should have custom mode + expect(content).toContain("custom") + }) + }) + + describe("message structure", () => { + it("should have valid message structure", async () => { + mockContext.args = ["code"] + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message).toHaveProperty("id") + expect(message).toHaveProperty("type") + expect(message).toHaveProperty("content") + expect(message).toHaveProperty("ts") + expect(typeof message.id).toBe("string") + expect(typeof message.type).toBe("string") + expect(typeof message.content).toBe("string") + expect(typeof message.ts).toBe("number") + }) + + it("should use current timestamp", async () => { + mockContext.args = ["code"] + const beforeTime = Date.now() + + await modeCommand.handler(mockContext) + + const message = mockAddMessage.mock.calls[0][0] + expect(message.ts).toBeGreaterThanOrEqual(beforeTime) + expect(message.ts).toBeLessThanOrEqual(Date.now()) + }) + }) +}) From a46b4c641c6601b467d7b0eb12ba246300fee28c Mon Sep 17 00:00:00 2001 From: Benson K B Date: Sun, 16 Nov 2025 16:10:08 +0530 Subject: [PATCH 7/9] fix: remove unnecessary semicolons from CLI types Clean up formatting to match project's no-semicolon style while maintaining the custom modes feature changes. --- cli/src/commands/core/types.ts | 24 ++-- cli/src/state/hooks/useCommandContext.ts | 140 +++++++++++------------ 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index 6d437c1538e..4f9b8da6f55 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -2,10 +2,10 @@ * Command system type definitions */ -import type { RouterModels, ModeConfig } from "../../types/messages.js"; -import type { CLIConfig, ProviderConfig } from "../../config/types.js"; -import type { ProfileData, BalanceData } from "../../state/atoms/profile.js"; -import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js"; +import type { RouterModels, ModeConfig } from "../../types/messages.js" +import type { CLIConfig, ProviderConfig } from "../../config/types.js" +import type { ProfileData, BalanceData } from "../../state/atoms/profile.js" +import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js" export interface Command { name: string @@ -17,7 +17,7 @@ export interface Command { handler: CommandHandler options?: CommandOption[] arguments?: ArgumentDefinition[] - priority?: number; // 1-10 scale, default 5. Higher = appears first in suggestions + priority?: number // 1-10 scale, default 5. Higher = appears first in suggestions } export interface CommandOption { @@ -74,7 +74,7 @@ export interface CommandContext { refreshTerminal: () => Promise } -export type CommandHandler = (context: CommandContext) => Promise | void; +export type CommandHandler = (context: CommandContext) => Promise | void export interface ParsedCommand { command: string @@ -118,7 +118,7 @@ export interface ArgumentProviderContext { parsedValues: { args: Record options: Record - }; + } // Metadata about the command command: Command @@ -134,7 +134,7 @@ export interface ArgumentProviderContext { updateProviderModel: (modelId: string) => Promise refreshRouterModels: () => Promise taskHistoryData: TaskHistoryData | null - }; + } } /** @@ -142,7 +142,7 @@ export interface ArgumentProviderContext { */ export type ArgumentProvider = ( context: ArgumentProviderContext, -) => Promise | ArgumentSuggestion[] | Promise | string[]; +) => Promise | ArgumentSuggestion[] | Promise | string[] /** * Validation result @@ -229,16 +229,16 @@ export interface InputState { definition: ArgumentDefinition index: number partialValue: string - }; + } validation?: { valid: boolean errors: string[] warnings: string[] - }; + } dependencies?: { satisfied: boolean missing: string[] - }; + } } diff --git a/cli/src/state/hooks/useCommandContext.ts b/cli/src/state/hooks/useCommandContext.ts index 3e59b0dae9b..98c9fcb392e 100644 --- a/cli/src/state/hooks/useCommandContext.ts +++ b/cli/src/state/hooks/useCommandContext.ts @@ -3,10 +3,10 @@ * Encapsulates all dependencies needed for command execution */ -import { useSetAtom, useAtomValue } from "jotai"; -import { useCallback } from "react"; -import type { CommandContext } from "../../commands/core/types.js"; -import type { CliMessage } from "../../types/cli.js"; +import { useSetAtom, useAtomValue } from "jotai" +import { useCallback } from "react" +import type { CommandContext } from "../../commands/core/types.js" +import type { CliMessage } from "../../types/cli.js" import { addMessageAtom, clearMessagesAtom, @@ -14,22 +14,22 @@ import { setMessageCutoffTimestampAtom, isCommittingParallelModeAtom, refreshTerminalAtom, -} from "../atoms/ui.js"; -import { setModeAtom, setThemeAtom, providerAtom, updateProviderAtom, configAtom } from "../atoms/config.js"; -import { routerModelsAtom, extensionStateAtom, isParallelModeAtom } from "../atoms/extension.js"; -import { requestRouterModelsAtom } from "../atoms/actions.js"; -import { profileDataAtom, balanceDataAtom, profileLoadingAtom, balanceLoadingAtom } from "../atoms/profile.js"; +} from "../atoms/ui.js" +import { setModeAtom, setThemeAtom, providerAtom, updateProviderAtom, configAtom } from "../atoms/config.js" +import { routerModelsAtom, extensionStateAtom, isParallelModeAtom } from "../atoms/extension.js" +import { requestRouterModelsAtom } from "../atoms/actions.js" +import { profileDataAtom, balanceDataAtom, profileLoadingAtom, balanceLoadingAtom } from "../atoms/profile.js" import { taskHistoryDataAtom, taskHistoryFiltersAtom, taskHistoryLoadingAtom, taskHistoryErrorAtom, -} from "../atoms/taskHistory.js"; -import { useWebviewMessage } from "./useWebviewMessage.js"; -import { useTaskHistory } from "./useTaskHistory.js"; -import { getModelIdKey } from "../../constants/providers/models.js"; +} from "../atoms/taskHistory.js" +import { useWebviewMessage } from "./useWebviewMessage.js" +import { useTaskHistory } from "./useTaskHistory.js" +import { getModelIdKey } from "../../constants/providers/models.js" -const TERMINAL_CLEAR_DELAY_MS = 500; +const TERMINAL_CLEAR_DELAY_MS = 500 /** * Factory function type for creating CommandContext @@ -39,14 +39,14 @@ export type CommandContextFactory = ( args: string[], options: Record, onExit: () => void, -) => CommandContext; +) => CommandContext /** * Return type for useCommandContext hook */ export interface UseCommandContextReturn { /** Factory function to create CommandContext objects */ - createContext: CommandContextFactory; + createContext: CommandContextFactory } /** @@ -58,56 +58,56 @@ export interface UseCommandContextReturn { * @example * ```tsx * function MyComponent() { - * const { createContext } = useCommandContext(); + * const { createContext } = useCommandContext() * * const handleCommand = (input: string, args: string[], options: Record) => { - * const context = createContext(input, args, options, onExit); - * await command.handler(context); - * }; + * const context = createContext(input, args, options, onExit) + * await command.handler(context) + * } * } * ``` */ export function useCommandContext(): UseCommandContextReturn { // Get atoms and hooks - const addMessage = useSetAtom(addMessageAtom); - const clearMessages = useSetAtom(clearMessagesAtom); - const replaceMessages = useSetAtom(replaceMessagesAtom); - const setMode = useSetAtom(setModeAtom); - const setTheme = useSetAtom(setThemeAtom); - const updateProvider = useSetAtom(updateProviderAtom); - const refreshRouterModels = useSetAtom(requestRouterModelsAtom); - const setMessageCutoffTimestamp = useSetAtom(setMessageCutoffTimestampAtom); - const setCommittingParallelMode = useSetAtom(isCommittingParallelModeAtom); - const refreshTerminal = useSetAtom(refreshTerminalAtom); - const { sendMessage, clearTask } = useWebviewMessage(); + const addMessage = useSetAtom(addMessageAtom) + const clearMessages = useSetAtom(clearMessagesAtom) + const replaceMessages = useSetAtom(replaceMessagesAtom) + const setMode = useSetAtom(setModeAtom) + const setTheme = useSetAtom(setThemeAtom) + const updateProvider = useSetAtom(updateProviderAtom) + const refreshRouterModels = useSetAtom(requestRouterModelsAtom) + const setMessageCutoffTimestamp = useSetAtom(setMessageCutoffTimestampAtom) + const setCommittingParallelMode = useSetAtom(isCommittingParallelModeAtom) + const refreshTerminal = useSetAtom(refreshTerminalAtom) + const { sendMessage, clearTask } = useWebviewMessage() // Get read-only state - const routerModels = useAtomValue(routerModelsAtom); - const currentProvider = useAtomValue(providerAtom); - const extensionState = useAtomValue(extensionStateAtom); - const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || ""; - const customModes = extensionState?.customModes || []; - const isParallelMode = useAtomValue(isParallelModeAtom); - const config = useAtomValue(configAtom); + const routerModels = useAtomValue(routerModelsAtom) + const currentProvider = useAtomValue(providerAtom) + const extensionState = useAtomValue(extensionStateAtom) + const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || "" + const customModes = extensionState?.customModes || [] + const isParallelMode = useAtomValue(isParallelModeAtom) + const config = useAtomValue(configAtom) // Get profile state - const profileData = useAtomValue(profileDataAtom); - const balanceData = useAtomValue(balanceDataAtom); - const profileLoading = useAtomValue(profileLoadingAtom); - const balanceLoading = useAtomValue(balanceLoadingAtom); + const profileData = useAtomValue(profileDataAtom) + const balanceData = useAtomValue(balanceDataAtom) + const profileLoading = useAtomValue(profileLoadingAtom) + const balanceLoading = useAtomValue(balanceLoadingAtom) // Get task history state and functions - const taskHistoryData = useAtomValue(taskHistoryDataAtom); - const taskHistoryFilters = useAtomValue(taskHistoryFiltersAtom); - const taskHistoryLoading = useAtomValue(taskHistoryLoadingAtom); - const taskHistoryError = useAtomValue(taskHistoryErrorAtom); + const taskHistoryData = useAtomValue(taskHistoryDataAtom) + const taskHistoryFilters = useAtomValue(taskHistoryFiltersAtom) + const taskHistoryLoading = useAtomValue(taskHistoryLoadingAtom) + const taskHistoryError = useAtomValue(taskHistoryErrorAtom) const { fetchTaskHistory, updateFilters: updateTaskHistoryFiltersAndFetch, changePage: changeTaskHistoryPageAndFetch, nextPage: nextTaskHistoryPage, previousPage: previousTaskHistoryPage, - } = useTaskHistory(); + } = useTaskHistory() // Create the factory function const createContext = useCallback( @@ -118,42 +118,42 @@ export function useCommandContext(): UseCommandContextReturn { options, config, sendMessage: async (message: any) => { - await sendMessage(message); + await sendMessage(message) }, addMessage: (message: CliMessage) => { - addMessage(message); + addMessage(message) }, clearMessages: () => { - clearMessages(); + clearMessages() }, refreshTerminal: () => { return new Promise((resolve) => { - refreshTerminal(); + refreshTerminal() setTimeout(() => { - resolve(); - }, TERMINAL_CLEAR_DELAY_MS); - }); + resolve() + }, TERMINAL_CLEAR_DELAY_MS) + }) }, replaceMessages: (messages: CliMessage[]) => { - replaceMessages(messages); + replaceMessages(messages) }, setMessageCutoffTimestamp: (timestamp: number) => { - setMessageCutoffTimestamp(timestamp); + setMessageCutoffTimestamp(timestamp) }, clearTask: async () => { - await clearTask(); + await clearTask() }, setMode: async (mode: string) => { - await setMode(mode); + await setMode(mode) }, setTheme: async (theme: string) => { - await setTheme(theme); + await setTheme(theme) }, exit: () => { - onExit(); + onExit() }, setCommittingParallelMode: (isCommitting: boolean) => { - setCommittingParallelMode(isCommitting); + setCommittingParallelMode(isCommitting) }, isParallelMode, // Model-related context @@ -162,20 +162,20 @@ export function useCommandContext(): UseCommandContextReturn { kilocodeDefaultModel, updateProviderModel: async (modelId: string) => { if (!currentProvider) { - throw new Error("No provider configured"); + throw new Error("No provider configured") } - const modelIdKey = getModelIdKey(currentProvider.provider); + const modelIdKey = getModelIdKey(currentProvider.provider) await updateProvider(currentProvider.id, { [modelIdKey]: modelId, - }); + }) }, refreshRouterModels: async () => { - await refreshRouterModels(); + await refreshRouterModels() }, // Provider update function for teams command updateProvider: async (providerId: string, updates: any) => { - await updateProvider(providerId, updates); + await updateProvider(providerId, updates) }, // Profile data context profileData, @@ -195,7 +195,7 @@ export function useCommandContext(): UseCommandContextReturn { nextTaskHistoryPage, previousTaskHistoryPage, sendWebviewMessage: sendMessage, - }; + } }, [ config, @@ -217,9 +217,9 @@ export function useCommandContext(): UseCommandContextReturn { balanceData, profileLoading, balanceLoading, - customModes, setCommittingParallelMode, isParallelMode, + customModes, taskHistoryData, taskHistoryFilters, taskHistoryLoading, @@ -230,7 +230,7 @@ export function useCommandContext(): UseCommandContextReturn { nextTaskHistoryPage, previousTaskHistoryPage, ], - ); + ) - return { createContext }; + return { createContext } } From d75302cbd2babfd1d3d00ff24398007812924712 Mon Sep 17 00:00:00 2001 From: Benson K B Date: Sun, 16 Nov 2025 16:36:41 +0530 Subject: [PATCH 8/9] fix: resolve lint errors and failing test in CLI - Remove unused imports from mode.ts (ArgumentValue, DEFAULT_MODES) - Replace any types with unknown and proper casting in customModes.ts - Rename unused error variables to _error - Add missing properties to mode.test.ts mock context - Treat undefined source as "global" for built-in modes --- cli/src/commands/__tests__/mode.test.ts | 4 +++- cli/src/commands/mode.ts | 8 ++++--- cli/src/config/customModes.ts | 32 +++++++++++++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cli/src/commands/__tests__/mode.test.ts b/cli/src/commands/__tests__/mode.test.ts index 865b25872a5..42c5c000c95 100644 --- a/cli/src/commands/__tests__/mode.test.ts +++ b/cli/src/commands/__tests__/mode.test.ts @@ -20,7 +20,7 @@ describe("modeCommand", () => { input: "/mode", args: [], options: {}, - config: {} as any, + config: {} as CommandContext["config"], sendMessage: vi.fn().mockResolvedValue(undefined), addMessage: mockAddMessage, clearMessages: vi.fn(), @@ -38,6 +38,7 @@ describe("modeCommand", () => { updateProviderModel: vi.fn().mockResolvedValue(undefined), refreshRouterModels: vi.fn().mockResolvedValue(undefined), updateProvider: vi.fn().mockResolvedValue(undefined), + selectProvider: vi.fn().mockResolvedValue(undefined), profileData: null, balanceData: null, profileLoading: false, @@ -58,6 +59,7 @@ describe("modeCommand", () => { previousTaskHistoryPage: vi.fn().mockResolvedValue(null), sendWebviewMessage: vi.fn().mockResolvedValue(undefined), refreshTerminal: vi.fn().mockResolvedValue(undefined), + chatMessages: [], } }) diff --git a/cli/src/commands/mode.ts b/cli/src/commands/mode.ts index f5dc238e95e..54cb30a5c8d 100644 --- a/cli/src/commands/mode.ts +++ b/cli/src/commands/mode.ts @@ -2,8 +2,8 @@ * /mode command - Switch between different modes */ -import type { Command, ArgumentValue } from "./core/types.js" -import { DEFAULT_MODES, getAllModes } from "../constants/modes/defaults.js" +import type { Command } from "./core/types.js" +import { getAllModes } from "../constants/modes/defaults.js" export const modeCommand: Command = { name: "mode", @@ -32,7 +32,9 @@ export const modeCommand: Command = { if (args.length === 0 || !args[0]) { // Show current mode and available modes const modesList = allModes.map((mode) => { - const source = mode.source === "project" ? " (project)" : mode.source === "global" ? " (global)" : "" + // Treat undefined source as "global" (for built-in modes from @roo-code/types) + const source = + mode.source === "project" ? " (project)" : mode.source === "global" || !mode.source ? " (global)" : "" return ` - **${mode.name}** (${mode.slug})${source}: ${mode.description || "No description"}` }) diff --git a/cli/src/config/customModes.ts b/cli/src/config/customModes.ts index 8f16fdbeb27..2e0101ea559 100644 --- a/cli/src/config/customModes.ts +++ b/cli/src/config/customModes.ts @@ -93,19 +93,25 @@ function parseCustomModes(content: string, source: "global" | "project"): ModeCo // Validate and normalize mode configs return modes - .filter((mode: any) => { + .filter((mode: unknown) => { // Must have at least slug and name - return mode && typeof mode === "object" && mode.slug && mode.name + const m = mode as Record + return m && typeof m === "object" && m.slug && m.name }) - .map((mode: any) => ({ - slug: mode.slug, - name: mode.name, - roleDefinition: mode.roleDefinition || mode.systemPrompt || "", - groups: mode.groups || ["read", "edit", "browser", "command", "mcp"], - customInstructions: mode.customInstructions || (mode.rules ? mode.rules.join("\n") : undefined), - source: mode.source || source, - })) - } catch (error) { + .map((mode: unknown) => { + const m = mode as Record + return { + slug: m.slug as string, + name: m.name as string, + roleDefinition: (m.roleDefinition as string) || (m.systemPrompt as string) || "", + groups: (m.groups as ModeConfig["groups"]) || ["read", "edit", "browser", "command", "mcp"], + customInstructions: + (m.customInstructions as string) || + (m.rules ? (m.rules as string[]).join("\n") : undefined), + source: (m.source as ModeConfig["source"]) || source, + } + }) + } catch (_error) { // Silent fail - return empty array if parsing fails return [] } @@ -125,7 +131,7 @@ async function loadGlobalCustomModes(): Promise { try { const content = await readFile(globalPath, "utf-8") return parseCustomModes(content, "global") - } catch (error) { + } catch (_error) { // Silent fail - return empty array if reading fails return [] } @@ -146,7 +152,7 @@ async function loadProjectCustomModes(workspace: string): Promise try { const content = await readFile(projectPath, "utf-8") return parseCustomModes(content, "project") - } catch (error) { + } catch (_error) { // Silent fail - return empty array if reading fails return [] } From df83fc71c9dcf4f8aaad0d55a0fd17732d493ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 25 Nov 2025 10:09:12 -0300 Subject: [PATCH 9/9] refactor: remove style changes & update the changeset --- .changeset/busy-deer-crash.md | 5 +++++ .changeset/custom-modes-support.md | 8 -------- 2 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 .changeset/busy-deer-crash.md delete mode 100644 .changeset/custom-modes-support.md diff --git a/.changeset/busy-deer-crash.md b/.changeset/busy-deer-crash.md new file mode 100644 index 00000000000..8f93209a5e8 --- /dev/null +++ b/.changeset/busy-deer-crash.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Custom modes support diff --git a/.changeset/custom-modes-support.md b/.changeset/custom-modes-support.md deleted file mode 100644 index ca383b6bfff..00000000000 --- a/.changeset/custom-modes-support.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -feat(cli): add custom modes support and refactor implementation - -This PR adds support for loading custom modes from both global and project-specific configuration files. Custom modes can be defined in YAML format and will be merged with default modes, with project modes taking precedence over global modes.