modes.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import * as vscode from "vscode"
  2. import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
  3. import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
  4. // Mode types
  5. export type Mode = string
  6. // Group options type
  7. export type GroupOptions = {
  8. fileRegex?: string // Regular expression pattern
  9. description?: string // Human-readable description of the pattern
  10. }
  11. // Group entry can be either a string or tuple with options
  12. export type GroupEntry = ToolGroup | readonly [ToolGroup, GroupOptions]
  13. // Mode configuration type
  14. export type ModeConfig = {
  15. slug: string
  16. name: string
  17. roleDefinition: string
  18. customInstructions?: string
  19. groups: readonly GroupEntry[] // Now supports both simple strings and tuples with options
  20. source?: "global" | "project" // Where this mode was loaded from
  21. }
  22. // Mode-specific prompts only
  23. export type PromptComponent = {
  24. roleDefinition?: string
  25. customInstructions?: string
  26. }
  27. export type CustomModePrompts = {
  28. [key: string]: PromptComponent | undefined
  29. }
  30. // Helper to extract group name regardless of format
  31. export function getGroupName(group: GroupEntry): ToolGroup {
  32. if (typeof group === "string") {
  33. return group
  34. }
  35. return group[0]
  36. }
  37. // Helper to get group options if they exist
  38. function getGroupOptions(group: GroupEntry): GroupOptions | undefined {
  39. return Array.isArray(group) ? group[1] : undefined
  40. }
  41. // Helper to check if a file path matches a regex pattern
  42. export function doesFileMatchRegex(filePath: string, pattern: string): boolean {
  43. try {
  44. const regex = new RegExp(pattern)
  45. return regex.test(filePath)
  46. } catch (error) {
  47. console.error(`Invalid regex pattern: ${pattern}`, error)
  48. return false
  49. }
  50. }
  51. // Helper to get all tools for a mode
  52. export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
  53. const tools = new Set<string>()
  54. // Add tools from each group
  55. groups.forEach((group) => {
  56. const groupName = getGroupName(group)
  57. const groupConfig = TOOL_GROUPS[groupName]
  58. groupConfig.tools.forEach((tool: string) => tools.add(tool))
  59. })
  60. // Always add required tools
  61. ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
  62. return Array.from(tools)
  63. }
  64. // Main modes configuration as an ordered array
  65. export const modes: readonly ModeConfig[] = [
  66. {
  67. slug: "code",
  68. name: "Code",
  69. roleDefinition:
  70. "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
  71. groups: ["read", "edit", "browser", "command", "mcp"],
  72. },
  73. {
  74. slug: "architect",
  75. name: "Architect",
  76. roleDefinition:
  77. "You are Roo, 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.",
  78. groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
  79. customInstructions:
  80. "1. Do some information gathering (for example using read_file or search_files) 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, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. 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 plan the best way to accomplish it.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.",
  81. },
  82. {
  83. slug: "ask",
  84. name: "Ask",
  85. roleDefinition:
  86. "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.",
  87. groups: ["read", "browser", "mcp"],
  88. customInstructions:
  89. "You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.",
  90. },
  91. {
  92. slug: "debug",
  93. name: "Debug",
  94. roleDefinition:
  95. "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.",
  96. groups: ["read", "edit", "browser", "command", "mcp"],
  97. customInstructions:
  98. "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.",
  99. },
  100. ] as const
  101. // Export the default mode slug
  102. export const defaultModeSlug = modes[0].slug
  103. // Helper functions
  104. export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined {
  105. // Check custom modes first
  106. const customMode = customModes?.find((mode) => mode.slug === slug)
  107. if (customMode) {
  108. return customMode
  109. }
  110. // Then check built-in modes
  111. return modes.find((mode) => mode.slug === slug)
  112. }
  113. export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig {
  114. const mode = getModeBySlug(slug, customModes)
  115. if (!mode) {
  116. throw new Error(`No mode found for slug: ${slug}`)
  117. }
  118. return mode
  119. }
  120. // Get all available modes, with custom modes overriding built-in modes
  121. export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
  122. if (!customModes?.length) {
  123. return [...modes]
  124. }
  125. // Start with built-in modes
  126. const allModes = [...modes]
  127. // Process custom modes
  128. customModes.forEach((customMode) => {
  129. const index = allModes.findIndex((mode) => mode.slug === customMode.slug)
  130. if (index !== -1) {
  131. // Override existing mode
  132. allModes[index] = customMode
  133. } else {
  134. // Add new mode
  135. allModes.push(customMode)
  136. }
  137. })
  138. return allModes
  139. }
  140. // Check if a mode is custom or an override
  141. export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean {
  142. return !!customModes?.some((mode) => mode.slug === slug)
  143. }
  144. // Custom error class for file restrictions
  145. export class FileRestrictionError extends Error {
  146. constructor(mode: string, pattern: string, description: string | undefined, filePath: string) {
  147. super(
  148. `This mode (${mode}) can only edit files matching pattern: ${pattern}${description ? ` (${description})` : ""}. Got: ${filePath}`,
  149. )
  150. this.name = "FileRestrictionError"
  151. }
  152. }
  153. export function isToolAllowedForMode(
  154. tool: string,
  155. modeSlug: string,
  156. customModes: ModeConfig[],
  157. toolRequirements?: Record<string, boolean>,
  158. toolParams?: Record<string, any>, // All tool parameters
  159. experiments?: Record<string, boolean>,
  160. ): boolean {
  161. // Always allow these tools
  162. if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
  163. return true
  164. }
  165. if (experiments && tool in experiments) {
  166. if (!experiments[tool]) {
  167. return false
  168. }
  169. }
  170. // Check tool requirements if any exist
  171. if (toolRequirements && tool in toolRequirements) {
  172. if (!toolRequirements[tool]) {
  173. return false
  174. }
  175. }
  176. const mode = getModeBySlug(modeSlug, customModes)
  177. if (!mode) {
  178. return false
  179. }
  180. // Check if tool is in any of the mode's groups and respects any group options
  181. for (const group of mode.groups) {
  182. const groupName = getGroupName(group)
  183. const options = getGroupOptions(group)
  184. const groupConfig = TOOL_GROUPS[groupName]
  185. // If the tool isn't in this group's tools, continue to next group
  186. if (!groupConfig.tools.includes(tool)) {
  187. continue
  188. }
  189. // If there are no options, allow the tool
  190. if (!options) {
  191. return true
  192. }
  193. // For the edit group, check file regex if specified
  194. if (groupName === "edit" && options.fileRegex) {
  195. const filePath = toolParams?.path
  196. if (
  197. filePath &&
  198. (toolParams.diff || toolParams.content || toolParams.operations) &&
  199. !doesFileMatchRegex(filePath, options.fileRegex)
  200. ) {
  201. throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath)
  202. }
  203. }
  204. return true
  205. }
  206. return false
  207. }
  208. // Create the mode-specific default prompts
  209. export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
  210. Object.fromEntries(
  211. modes.map((mode) => [
  212. mode.slug,
  213. {
  214. roleDefinition: mode.roleDefinition,
  215. customInstructions: mode.customInstructions,
  216. },
  217. ]),
  218. ),
  219. )
  220. // Helper function to get all modes with their prompt overrides from extension state
  221. export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise<ModeConfig[]> {
  222. const customModes = (await context.globalState.get<ModeConfig[]>("customModes")) || []
  223. const customModePrompts = (await context.globalState.get<CustomModePrompts>("customModePrompts")) || {}
  224. const allModes = getAllModes(customModes)
  225. return allModes.map((mode) => ({
  226. ...mode,
  227. roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition,
  228. customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions,
  229. }))
  230. }
  231. // Helper function to get complete mode details with all overrides
  232. export async function getFullModeDetails(
  233. modeSlug: string,
  234. customModes?: ModeConfig[],
  235. customModePrompts?: CustomModePrompts,
  236. options?: {
  237. cwd?: string
  238. globalCustomInstructions?: string
  239. preferredLanguage?: string
  240. },
  241. ): Promise<ModeConfig> {
  242. // First get the base mode config from custom modes or built-in modes
  243. const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0]
  244. // Check for any prompt component overrides
  245. const promptComponent = customModePrompts?.[modeSlug]
  246. // Get the base custom instructions
  247. const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || ""
  248. // If we have cwd, load and combine all custom instructions
  249. let fullCustomInstructions = baseCustomInstructions
  250. if (options?.cwd) {
  251. fullCustomInstructions = await addCustomInstructions(
  252. baseCustomInstructions,
  253. options.globalCustomInstructions || "",
  254. options.cwd,
  255. modeSlug,
  256. { preferredLanguage: options.preferredLanguage },
  257. )
  258. }
  259. // Return mode with any overrides applied
  260. return {
  261. ...baseMode,
  262. roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition,
  263. customInstructions: fullCustomInstructions,
  264. }
  265. }
  266. // Helper function to safely get role definition
  267. export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string {
  268. const mode = getModeBySlug(modeSlug, customModes)
  269. if (!mode) {
  270. console.warn(`No mode found for slug: ${modeSlug}`)
  271. return ""
  272. }
  273. return mode.roleDefinition
  274. }
  275. // Helper function to safely get custom instructions
  276. export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig[]): string {
  277. const mode = getModeBySlug(modeSlug, customModes)
  278. if (!mode) {
  279. console.warn(`No mode found for slug: ${modeSlug}`)
  280. return ""
  281. }
  282. return mode.customInstructions ?? ""
  283. }