Custom modes

This commit is contained in:
Matt Rubens
2025-01-18 03:39:26 -05:00
parent 332245c33a
commit b8e0aa0cde
65 changed files with 3749 additions and 1531 deletions

View File

@@ -4,7 +4,14 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp"
import { GitCommit } from "../utils/git"
import { Mode, CustomPrompts } from "./modes"
import { Mode, CustomPrompts, ModeConfig } from "./modes"
export interface LanguageModelChatSelector {
vendor?: string
family?: string
version?: string
id?: string
}
// webview will hold state
export interface ExtensionMessage {
@@ -31,6 +38,8 @@ export interface ExtensionMessage {
| "updatePrompt"
| "systemPrompt"
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
text?: string
action?:
| "chatButtonClicked"
@@ -54,6 +63,8 @@ export interface ExtensionMessage {
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]
mode?: Mode
customMode?: ModeConfig
slug?: string
}
export interface ApiConfigMeta {
@@ -96,6 +107,7 @@ export interface ExtensionState {
enhancementApiConfigId?: string
experimentalDiffStrategy?: boolean
autoApprovalEnabled?: boolean
customModes: ModeConfig[]
}
export interface ClineMessage {

View File

@@ -1,5 +1,5 @@
import { ApiConfiguration, ApiProvider } from "./api"
import { Mode, PromptComponent } from "./modes"
import { Mode, PromptComponent, ModeConfig } from "./modes"
export type PromptMode = Mode | "enhance"
@@ -74,6 +74,8 @@ export interface WebviewMessage {
| "enhancementApiConfigId"
| "experimentalDiffStrategy"
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
@@ -92,6 +94,8 @@ export interface WebviewMessage {
dataUrls?: string[]
values?: Record<string, any>
query?: string
slug?: string
modeConfig?: ModeConfig
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

View File

@@ -1,10 +1,4 @@
// Tool options for specific tools
export type ToolOptions = {
string: readonly string[]
}
// Tool configuration tuple type
export type ToolConfig = readonly [string] | readonly [string, ToolOptions]
import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
// Mode types
export type Mode = string
@@ -14,7 +8,124 @@ export type ModeConfig = {
slug: string
name: string
roleDefinition: string
tools: readonly ToolConfig[]
customInstructions?: string
groups: readonly ToolGroup[] // Now uses groups instead of tools array
}
// Helper to get all tools for a mode
export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
const tools = new Set<string>()
// Add tools from each group
groups.forEach((group) => {
TOOL_GROUPS[group].forEach((tool) => tools.add(tool))
})
// Always add required tools
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
return Array.from(tools)
}
// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
{
slug: "code",
name: "Code",
roleDefinition:
"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
groups: ["read", "edit", "browser", "command", "mcp"],
},
{
slug: "architect",
name: "Architect",
roleDefinition:
"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
groups: ["read", "browser", "mcp"],
},
{
slug: "ask",
name: "Ask",
roleDefinition:
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
groups: ["read", "browser", "mcp"],
},
] as const
// Export the default mode slug
export const defaultModeSlug = modes[0].slug
// Helper functions
export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined {
// Check custom modes first
const customMode = customModes?.find((mode) => mode.slug === slug)
if (customMode) {
return customMode
}
// Then check built-in modes
return modes.find((mode) => mode.slug === slug)
}
export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig {
const mode = getModeBySlug(slug, customModes)
if (!mode) {
throw new Error(`No mode found for slug: ${slug}`)
}
return mode
}
// Get all available modes, with custom modes overriding built-in modes
export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
if (!customModes?.length) {
return [...modes]
}
// Start with built-in modes
const allModes = [...modes]
// Process custom modes
customModes.forEach((customMode) => {
const index = allModes.findIndex((mode) => mode.slug === customMode.slug)
if (index !== -1) {
// Override existing mode
allModes[index] = customMode
} else {
// Add new mode
allModes.push(customMode)
}
})
return allModes
}
// Check if a mode is custom or an override
export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean {
return !!customModes?.some((mode) => mode.slug === slug)
}
export function isToolAllowedForMode(tool: string, modeSlug: string, customModes: ModeConfig[]): boolean {
// Always allow these tools
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
return true
}
const mode = getModeBySlug(modeSlug, customModes)
if (!mode) {
return false
}
// Check if tool is in any of the mode's groups
return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string))
}
export type PromptComponent = {
roleDefinition?: string
customInstructions?: string
}
// Mode-specific prompts only
export type CustomPrompts = {
[key: string]: PromptComponent | undefined
}
// Separate enhance prompt type and definition
@@ -26,127 +137,25 @@ export const enhance: EnhanceConfig = {
prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):",
} as const
// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
{
slug: "code",
name: "Code",
roleDefinition:
"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
tools: [
["execute_command"],
["read_file"],
["write_to_file"],
["apply_diff"],
["search_files"],
["list_files"],
["list_code_definition_names"],
["browser_action"],
["use_mcp_tool"],
["access_mcp_resource"],
["ask_followup_question"],
["attempt_completion"],
] as const,
// Completely separate enhance prompt handling
export const enhancePrompt = {
default: enhance.prompt,
get: (customPrompts: Record<string, any> | undefined): string => {
return customPrompts?.enhance ?? enhance.prompt
},
{
slug: "architect",
name: "Architect",
roleDefinition:
"You are Cline, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
tools: [
["read_file"],
["search_files"],
["list_files"],
["list_code_definition_names"],
["browser_action"],
["use_mcp_tool"],
["access_mcp_resource"],
["ask_followup_question"],
["attempt_completion"],
] as const,
},
{
slug: "ask",
name: "Ask",
roleDefinition:
"You are Cline, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
tools: [
["read_file"],
["search_files"],
["list_files"],
["list_code_definition_names"],
["browser_action"],
["use_mcp_tool"],
["access_mcp_resource"],
["ask_followup_question"],
["attempt_completion"],
] as const,
},
] as const
// Export the default mode slug
export const defaultModeSlug = modes[0].slug
// Helper functions
export function getModeBySlug(slug: string): ModeConfig | undefined {
return modes.find((mode) => mode.slug === slug)
}
export function getModeConfig(slug: string): ModeConfig {
const mode = getModeBySlug(slug)
if (!mode) {
throw new Error(`No mode found for slug: ${slug}`)
}
return mode
}
// Derive tool names from the modes configuration
export type ToolName = (typeof modes)[number]["tools"][number][0]
export type TestToolName = ToolName | "unknown_tool"
export function isToolAllowedForMode(tool: TestToolName, modeSlug: string): boolean {
if (tool === "unknown_tool") {
return false
}
const mode = getModeBySlug(modeSlug)
if (!mode) {
return false
}
return mode.tools.some(([toolName]) => toolName === tool)
}
export function getToolOptions(tool: ToolName, modeSlug: string): ToolOptions | undefined {
const mode = getModeBySlug(modeSlug)
if (!mode) {
return undefined
}
const toolConfig = mode.tools.find(([toolName]) => toolName === tool)
return toolConfig?.[1]
}
export type PromptComponent = {
roleDefinition?: string
customInstructions?: string
}
export type CustomPrompts = {
[key: string]: PromptComponent | string | undefined
}
// Create the defaultPrompts object with the correct type
export const defaultPrompts: CustomPrompts = {
...Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
enhance: enhance.prompt,
} as const
// Create the mode-specific default prompts
export const defaultPrompts: Readonly<CustomPrompts> = Object.freeze(
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
)
// Helper function to safely get role definition
export function getRoleDefinition(modeSlug: string): string {
const prompt = defaultPrompts[modeSlug]
if (!prompt || typeof prompt === "string") {
throw new Error(`Invalid mode slug: ${modeSlug}`)
export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string {
const mode = getModeBySlug(modeSlug, customModes)
if (!mode) {
console.warn(`No mode found for slug: ${modeSlug}`)
return ""
}
if (!prompt.roleDefinition) {
throw new Error(`No role definition found for mode: ${modeSlug}`)
}
return prompt.roleDefinition
return mode.roleDefinition
}

53
src/shared/tool-groups.ts Normal file
View File

@@ -0,0 +1,53 @@
// Define tool group values
export type ToolGroupValues = readonly string[]
// Map of tool slugs to their display names
export const TOOL_DISPLAY_NAMES = {
execute_command: "run commands",
read_file: "read files",
write_to_file: "write files",
apply_diff: "apply changes",
search_files: "search files",
list_files: "list files",
list_code_definition_names: "list definitions",
browser_action: "use a browser",
use_mcp_tool: "use mcp tools",
access_mcp_resource: "access mcp resources",
ask_followup_question: "ask questions",
attempt_completion: "complete tasks",
} as const
// Define available tool groups
export const TOOL_GROUPS: Record<string, ToolGroupValues> = {
read: ["read_file", "search_files", "list_files", "list_code_definition_names"],
edit: ["write_to_file", "apply_diff"],
browser: ["browser_action"],
command: ["execute_command"],
mcp: ["use_mcp_tool", "access_mcp_resource"],
}
export type ToolGroup = keyof typeof TOOL_GROUPS
// Tools that are always available to all modes
export const ALWAYS_AVAILABLE_TOOLS = ["ask_followup_question", "attempt_completion"] as const
// Tool name types for type safety
export type ToolName = keyof typeof TOOL_DISPLAY_NAMES
// Tool helper functions
export function getToolName(toolConfig: string | readonly [ToolName, ...any[]]): ToolName {
return typeof toolConfig === "string" ? (toolConfig as ToolName) : toolConfig[0]
}
export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]]): any {
return typeof toolConfig === "string" ? undefined : toolConfig[1]
}
// Display names for groups in UI
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
read: "Read Files",
edit: "Edit Files",
browser: "Use Browser",
command: "Run Commands",
mcp: "Use MCP",
}