From 62fd24453abc4a0aeb93d8299e95d019c2bfd637 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 29 Jul 2025 07:53:59 +0000 Subject: [PATCH] feat: Phase 3 - Update localization files for mode to agent rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update English localization keys from modes.* to agents.* - Update all 17 language localizations systematically - Preserve marketplace-related "mode" terminology as required - Update component references to use new localization keys - Convert customModes to customAgents in localization - Update deleteMode to deleteAgent prompts - Update mode_exported/imported to agent_exported/imported - Maintain backward compatibility where needed Languages updated: - English (en) - Primary reference - French (fr), German (de), Spanish (es) - Japanese (ja), Chinese Simplified (zh-CN), Chinese Traditional (zh-TW) - Korean (ko), Portuguese Brazil (pt-BR), Russian (ru) - Italian (it), Dutch (nl), Polish (pl), Turkish (tr) - Hindi (hi), Indonesian (id), Vietnamese (vi), Catalan (ca) Key changes: - customModes → customAgents - deleteMode → deleteAgent - mode_exported → agent_exported - mode_imported → agent_imported - retrieve_current_mode → retrieve_current_agent - Marketplace filter "mode" preserved as required --- packages/types/src/mode.ts | 66 +- src/core/config/CustomAgentsManager.ts | 1240 +++++++++++++ src/core/config/importExport.ts | 12 +- src/core/webview/ClineProvider.ts | 22 +- src/core/webview/webviewMessageHandler.ts | 26 +- src/i18n/locales/ca/common.json | 42 +- src/i18n/locales/ca/marketplace.json | 6 +- src/i18n/locales/de/common.json | 34 +- src/i18n/locales/de/marketplace.json | 4 +- src/i18n/locales/en/common.json | 42 +- src/i18n/locales/es/common.json | 22 +- src/i18n/locales/es/marketplace.json | 4 +- src/i18n/locales/fr/common.json | 42 +- src/i18n/locales/fr/marketplace.json | 4 +- src/i18n/locales/hi/common.json | 18 +- src/i18n/locales/hi/marketplace.json | 6 +- src/i18n/locales/id/common.json | 42 +- src/i18n/locales/id/marketplace.json | 6 +- src/i18n/locales/it/common.json | 18 +- src/i18n/locales/it/marketplace.json | 6 +- src/i18n/locales/ja/common.json | 34 +- src/i18n/locales/ja/marketplace.json | 4 +- src/i18n/locales/ko/common.json | 18 +- src/i18n/locales/ko/marketplace.json | 6 +- src/i18n/locales/nl/common.json | 18 +- src/i18n/locales/nl/marketplace.json | 6 +- src/i18n/locales/pl/common.json | 18 +- src/i18n/locales/pl/marketplace.json | 6 +- src/i18n/locales/pt-BR/common.json | 18 +- src/i18n/locales/pt-BR/marketplace.json | 6 +- src/i18n/locales/ru/common.json | 18 +- src/i18n/locales/ru/marketplace.json | 6 +- src/i18n/locales/tr/common.json | 18 +- src/i18n/locales/tr/marketplace.json | 6 +- src/i18n/locales/vi/common.json | 18 +- src/i18n/locales/vi/marketplace.json | 6 +- src/i18n/locales/zh-CN/common.json | 42 +- src/i18n/locales/zh-CN/marketplace.json | 4 +- src/i18n/locales/zh-TW/common.json | 18 +- src/i18n/locales/zh-TW/marketplace.json | 6 +- .../marketplace/MarketplaceManager.ts | 39 +- src/services/marketplace/SimpleInstaller.ts | 32 +- src/shared/ExtensionMessage.ts | 9 + src/shared/WebviewMessage.ts | 11 +- src/shared/modes.ts | 394 ++-- webview-ui/src/App.tsx | 8 +- .../src/components/agents/AgentsView.tsx | 1653 +++++++++++++++++ .../components/agents/DeleteAgentDialog.tsx | 61 + .../agents/__tests__/AgentsView.spec.tsx | 267 +++ .../src/components/chat/AgentSelector.tsx | 304 +++ .../src/components/chat/ChatTextArea.tsx | 20 +- .../src/components/chat/EditAgentControls.tsx | 115 ++ ...lector.spec.tsx => AgentSelector.spec.tsx} | 53 +- ...ls.spec.tsx => EditAgentControls.spec.tsx} | 22 +- .../components/MarketplaceInstallModal.tsx | 4 +- .../src/context/ExtensionStateContext.tsx | 14 + 56 files changed, 4410 insertions(+), 534 deletions(-) create mode 100644 src/core/config/CustomAgentsManager.ts create mode 100644 webview-ui/src/components/agents/AgentsView.tsx create mode 100644 webview-ui/src/components/agents/DeleteAgentDialog.tsx create mode 100644 webview-ui/src/components/agents/__tests__/AgentsView.spec.tsx create mode 100644 webview-ui/src/components/chat/AgentSelector.tsx create mode 100644 webview-ui/src/components/chat/EditAgentControls.tsx rename webview-ui/src/components/chat/__tests__/{ModeSelector.spec.tsx => AgentSelector.spec.tsx} (73%) rename webview-ui/src/components/chat/__tests__/{EditModeControls.spec.tsx => EditAgentControls.spec.tsx} (84%) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 88dcbb9574..b59fdd20c5 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -39,7 +39,7 @@ export const groupEntrySchema = z.union([toolGroupsSchema, z.tuple([toolGroupsSc export type GroupEntry = z.infer /** - * ModeConfig + * AgentConfig (new primary type) */ const groupEntryArraySchema = z.array(groupEntrySchema).refine( @@ -61,7 +61,7 @@ const groupEntryArraySchema = z.array(groupEntrySchema).refine( { message: "Duplicate groups are not allowed" }, ) -export const modeConfigSchema = z.object({ +export const agentConfigSchema = z.object({ slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"), name: z.string().min(1, "Name is required"), roleDefinition: z.string().min(1, "Role definition is required"), @@ -72,10 +72,44 @@ export const modeConfigSchema = z.object({ source: z.enum(["global", "project"]).optional(), }) -export type ModeConfig = z.infer +export type AgentConfig = z.infer /** - * CustomModesSettings + * ModeConfig (backward compatibility alias) + */ + +export const modeConfigSchema = agentConfigSchema + +export type ModeConfig = AgentConfig + +/** + * CustomAgentsSettings (new primary type) + */ + +export const customAgentsSettingsSchema = z.object({ + customAgents: z.array(agentConfigSchema).refine( + (agents) => { + const slugs = new Set() + + return agents.every((agent) => { + if (slugs.has(agent.slug)) { + return false + } + + slugs.add(agent.slug) + return true + }) + }, + { + message: "Duplicate agent slugs are not allowed", + }, + ), +}) + +export type CustomAgentsSettings = z.infer + +/** + * CustomModesSettings (backward compatibility alias) */ export const customModesSettingsSchema = z.object({ @@ -114,12 +148,20 @@ export const promptComponentSchema = z.object({ export type PromptComponent = z.infer /** - * CustomModePrompts + * CustomAgentPrompts (new primary type) */ -export const customModePromptsSchema = z.record(z.string(), promptComponentSchema.optional()) +export const customAgentPromptsSchema = z.record(z.string(), promptComponentSchema.optional()) -export type CustomModePrompts = z.infer +export type CustomAgentPrompts = z.infer + +/** + * CustomModePrompts (backward compatibility alias) + */ + +export const customModePromptsSchema = customAgentPromptsSchema + +export type CustomModePrompts = CustomAgentPrompts /** * CustomSupportPrompts @@ -130,10 +172,10 @@ export const customSupportPromptsSchema = z.record(z.string(), z.string().option export type CustomSupportPrompts = z.infer /** - * DEFAULT_MODES + * DEFAULT_AGENTS (new primary constant) */ -export const DEFAULT_MODES: readonly ModeConfig[] = [ +export const DEFAULT_AGENTS: readonly AgentConfig[] = [ { slug: "architect", name: "🏗️ Architect", @@ -193,3 +235,9 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ "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 a subtask rather than overloading the current one.", }, ] as const + +/** + * DEFAULT_MODES (backward compatibility alias) + */ + +export const DEFAULT_MODES: readonly ModeConfig[] = DEFAULT_AGENTS diff --git a/src/core/config/CustomAgentsManager.ts b/src/core/config/CustomAgentsManager.ts new file mode 100644 index 0000000000..3835f0ff58 --- /dev/null +++ b/src/core/config/CustomAgentsManager.ts @@ -0,0 +1,1240 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" +import * as os from "os" + +import * as yaml from "yaml" +import stripBom from "strip-bom" + +import { + type AgentConfig, + type ModeConfig, + type PromptComponent, + customAgentsSettingsSchema, + customModesSettingsSchema, + agentConfigSchema, + modeConfigSchema, +} from "@roo-code/types" + +import { fileExistsAtPath } from "../../utils/fs" +import { getWorkspacePath } from "../../utils/path" +import { getGlobalRooDirectory } from "../../services/roo-config" +import { logger } from "../../utils/logging" +import { GlobalFileNames } from "../../shared/globalFileNames" +import { ensureSettingsDirectoryExists } from "../../utils/globalContext" +import { t } from "../../i18n" + +const ROOAGENTS_FILENAME = ".rooagents" +const ROOMODES_FILENAME = ".roomodes" // Backward compatibility + +// Type definitions for import/export functionality +interface RuleFile { + relativePath: string + content: string +} + +interface ExportedAgentConfig extends AgentConfig { + rulesFiles?: RuleFile[] +} + +interface ExportedModeConfig extends ModeConfig { + rulesFiles?: RuleFile[] +} + +interface ImportData { + customAgents?: ExportedAgentConfig[] + customModes?: ExportedModeConfig[] // Backward compatibility +} + +interface ExportResult { + success: boolean + yaml?: string + error?: string +} + +interface ImportResult { + success: boolean + error?: string +} + +export class CustomAgentsManager { + private static readonly cacheTTL = 10_000 + + private disposables: vscode.Disposable[] = [] + private isWriting = false + private writeQueue: Array<() => Promise> = [] + private cachedAgents: AgentConfig[] | null = null + private cachedAt: number = 0 + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly onUpdate: () => Promise, + ) { + this.watchCustomAgentsFiles().catch((error) => { + console.error("[CustomAgentsManager] Failed to setup file watchers:", error) + }) + } + + private async queueWrite(operation: () => Promise): Promise { + this.writeQueue.push(operation) + + if (!this.isWriting) { + await this.processWriteQueue() + } + } + + private async processWriteQueue(): Promise { + if (this.isWriting || this.writeQueue.length === 0) { + return + } + + this.isWriting = true + + try { + while (this.writeQueue.length > 0) { + const operation = this.writeQueue.shift() + + if (operation) { + await operation() + } + } + } finally { + this.isWriting = false + } + } + + private async getWorkspaceRooagents(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + + if (!workspaceFolders || workspaceFolders.length === 0) { + return undefined + } + + const workspaceRoot = getWorkspacePath() + const rooagentsPath = path.join(workspaceRoot, ROOAGENTS_FILENAME) + const exists = await fileExistsAtPath(rooagentsPath) + return exists ? rooagentsPath : undefined + } + + private async getWorkspaceRoomodes(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + + if (!workspaceFolders || workspaceFolders.length === 0) { + return undefined + } + + const workspaceRoot = getWorkspacePath() + const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME) + const exists = await fileExistsAtPath(roomodesPath) + return exists ? roomodesPath : undefined + } + + /** + * Regex pattern for problematic characters that need to be cleaned from YAML content + * Includes: + * - \u00A0: Non-breaking space + * - \u200B-\u200D: Zero-width spaces and joiners + * - \u2010-\u2015, \u2212: Various dash characters + * - \u2018-\u2019: Smart single quotes + * - \u201C-\u201D: Smart double quotes + */ + private static readonly PROBLEMATIC_CHARS_REGEX = + // eslint-disable-next-line no-misleading-character-class + /[\u00A0\u200B\u200C\u200D\u2010\u2011\u2012\u2013\u2014\u2015\u2212\u2018\u2019\u201C\u201D]/g + + /** + * Clean invisible and problematic characters from YAML content + */ + private cleanInvisibleCharacters(content: string): string { + // Single pass replacement for all problematic characters + return content.replace(CustomAgentsManager.PROBLEMATIC_CHARS_REGEX, (match) => { + switch (match) { + case "\u00A0": // Non-breaking space + return " " + case "\u200B": // Zero-width space + case "\u200C": // Zero-width non-joiner + case "\u200D": // Zero-width joiner + return "" + case "\u2018": // Left single quotation mark + case "\u2019": // Right single quotation mark + return "'" + case "\u201C": // Left double quotation mark + case "\u201D": // Right double quotation mark + return '"' + default: // Dash characters (U+2010 through U+2015, U+2212) + return "-" + } + }) + } + + /** + * Parse YAML content with enhanced error handling and preprocessing + */ + private parseYamlSafely(content: string, filePath: string): any { + // Clean the content + let cleanedContent = stripBom(content) + cleanedContent = this.cleanInvisibleCharacters(cleanedContent) + + try { + const parsed = yaml.parse(cleanedContent) + // Ensure we never return null or undefined + return parsed ?? {} + } catch (yamlError) { + // For .rooagents and .roomodes files, try JSON as fallback + if (filePath.endsWith(ROOAGENTS_FILENAME) || filePath.endsWith(ROOMODES_FILENAME)) { + try { + // Try parsing the original content as JSON (not the cleaned content) + return JSON.parse(content) + } catch (jsonError) { + // JSON also failed, show the original YAML error + const errorMsg = yamlError instanceof Error ? yamlError.message : String(yamlError) + console.error(`[CustomAgentsManager] Failed to parse YAML from ${filePath}:`, errorMsg) + + const lineMatch = errorMsg.match(/at line (\d+)/) + const line = lineMatch ? lineMatch[1] : "unknown" + vscode.window.showErrorMessage(t("common:customAgents.errors.yamlParseError", { line })) + + // Return empty object to prevent duplicate error handling + return {} + } + } + + // For non-.rooagents/.roomodes files, just log and return empty object + const errorMsg = yamlError instanceof Error ? yamlError.message : String(yamlError) + console.error(`[CustomAgentsManager] Failed to parse YAML from ${filePath}:`, errorMsg) + return {} + } + } + + private async loadAgentsFromFile(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, "utf-8") + const settings = this.parseYamlSafely(content, filePath) + + // Handle both new .rooagents format and legacy .roomodes format + let agentsArray: any[] = [] + let validationSchema: any + + if (settings.customAgents) { + // New .rooagents format + agentsArray = settings.customAgents + validationSchema = customAgentsSettingsSchema + } else if (settings.customModes) { + // Legacy .roomodes format - treat modes as agents + agentsArray = settings.customModes + validationSchema = customModesSettingsSchema + } else { + return [] + } + + const result = validationSchema.safeParse(settings) + + if (!result.success) { + console.error(`[CustomAgentsManager] Schema validation failed for ${filePath}:`, result.error) + + // Show user-friendly error for .rooagents/.roomodes files + if (filePath.endsWith(ROOAGENTS_FILENAME) || filePath.endsWith(ROOMODES_FILENAME)) { + const issues = result.error.issues + .map((issue: any) => `• ${issue.path.join(".")}: ${issue.message}`) + .join("\n") + + vscode.window.showErrorMessage(t("common:customAgents.errors.schemaValidationError", { issues })) + } + + return [] + } + + // Determine source based on file path + const isProjectFile = filePath.endsWith(ROOAGENTS_FILENAME) || filePath.endsWith(ROOMODES_FILENAME) + const source = isProjectFile ? ("project" as const) : ("global" as const) + + // Add source to each agent + return agentsArray.map((agent) => ({ ...agent, source })) + } catch (error) { + // Only log if the error wasn't already handled in parseYamlSafely + if (!(error as any).alreadyHandled) { + const errorMsg = `Failed to load agents from ${filePath}: ${error instanceof Error ? error.message : String(error)}` + console.error(`[CustomAgentsManager] ${errorMsg}`) + } + return [] + } + } + + private async mergeCustomAgents(projectAgents: AgentConfig[], globalAgents: AgentConfig[]): Promise { + const slugs = new Set() + const merged: AgentConfig[] = [] + + // Add project agents (takes precedence) + for (const agent of projectAgents) { + if (!slugs.has(agent.slug)) { + slugs.add(agent.slug) + merged.push({ ...agent, source: "project" }) + } + } + + // Add non-duplicate global agents + for (const agent of globalAgents) { + if (!slugs.has(agent.slug)) { + slugs.add(agent.slug) + merged.push({ ...agent, source: "global" }) + } + } + + return merged + } + + public async getCustomAgentsFilePath(): Promise { + const settingsDir = await ensureSettingsDirectoryExists(this.context) + const filePath = path.join(settingsDir, GlobalFileNames.customModes) // Keep using customModes for global settings + const fileExists = await fileExistsAtPath(filePath) + + if (!fileExists) { + await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 }))) + } + + return filePath + } + + private async watchCustomAgentsFiles(): Promise { + // Skip if test environment is detected + if (process.env.NODE_ENV === "test") { + return + } + + const settingsPath = await this.getCustomAgentsFilePath() + + // Watch settings file + const settingsWatcher = vscode.workspace.createFileSystemWatcher(settingsPath) + + const handleSettingsChange = async () => { + try { + // Ensure that the settings file exists (especially important for delete events) + await this.getCustomAgentsFilePath() + const content = await fs.readFile(settingsPath, "utf-8") + + const errorMessage = t("common:customAgents.errors.invalidFormat") + + let config: any + + try { + config = this.parseYamlSafely(content, settingsPath) + } catch (error) { + console.error(error) + vscode.window.showErrorMessage(errorMessage) + return + } + + const result = customModesSettingsSchema.safeParse(config) // Use legacy schema for global settings + + if (!result.success) { + vscode.window.showErrorMessage(errorMessage) + return + } + + // Get agents from .rooagents or .roomodes if they exist (takes precedence) + const rooagentsPath = await this.getWorkspaceRooagents() + const roomodesPath = await this.getWorkspaceRoomodes() + + let projectAgents: AgentConfig[] = [] + if (rooagentsPath) { + projectAgents = await this.loadAgentsFromFile(rooagentsPath) + } else if (roomodesPath) { + projectAgents = await this.loadAgentsFromFile(roomodesPath) + } + + // Merge agents from both sources (project takes precedence) + const mergedAgents = await this.mergeCustomAgents(projectAgents, result.data.customModes) + await this.context.globalState.update("customModes", mergedAgents) // Keep using customModes key for backward compatibility + this.clearCache() + await this.onUpdate() + } catch (error) { + console.error(`[CustomAgentsManager] Error handling settings file change:`, error) + } + } + + this.disposables.push(settingsWatcher.onDidChange(handleSettingsChange)) + this.disposables.push(settingsWatcher.onDidCreate(handleSettingsChange)) + this.disposables.push(settingsWatcher.onDidDelete(handleSettingsChange)) + this.disposables.push(settingsWatcher) + + // Watch .rooagents and .roomodes files - watch the paths even if they don't exist yet + const workspaceFolders = vscode.workspace.workspaceFolders + if (workspaceFolders && workspaceFolders.length > 0) { + const workspaceRoot = getWorkspacePath() + const rooagentsPath = path.join(workspaceRoot, ROOAGENTS_FILENAME) + const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME) + + // Watch .rooagents file + const rooagentsWatcher = vscode.workspace.createFileSystemWatcher(rooagentsPath) + // Watch .roomodes file for backward compatibility + const roomodesWatcher = vscode.workspace.createFileSystemWatcher(roomodesPath) + + const handleProjectFileChange = async () => { + try { + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + + // Check .rooagents first, then .roomodes for backward compatibility + let projectAgents: AgentConfig[] = [] + const rooagentsExists = await fileExistsAtPath(rooagentsPath) + const roomodesExists = await fileExistsAtPath(roomodesPath) + + if (rooagentsExists) { + projectAgents = await this.loadAgentsFromFile(rooagentsPath) + } else if (roomodesExists) { + projectAgents = await this.loadAgentsFromFile(roomodesPath) + } + + // Project agents take precedence + const mergedAgents = await this.mergeCustomAgents(projectAgents, settingsAgents) + await this.context.globalState.update("customModes", mergedAgents) // Keep using customModes key + this.clearCache() + await this.onUpdate() + } catch (error) { + console.error(`[CustomAgentsManager] Error handling project file change:`, error) + } + } + + const handleProjectFileDelete = async () => { + // When project files are deleted, refresh with only settings agents + try { + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + await this.context.globalState.update("customModes", settingsAgents) + this.clearCache() + await this.onUpdate() + } catch (error) { + console.error(`[CustomAgentsManager] Error handling project file deletion:`, error) + } + } + + // Set up watchers for both .rooagents and .roomodes + this.disposables.push(rooagentsWatcher.onDidChange(handleProjectFileChange)) + this.disposables.push(rooagentsWatcher.onDidCreate(handleProjectFileChange)) + this.disposables.push(rooagentsWatcher.onDidDelete(handleProjectFileDelete)) + this.disposables.push(rooagentsWatcher) + + this.disposables.push(roomodesWatcher.onDidChange(handleProjectFileChange)) + this.disposables.push(roomodesWatcher.onDidCreate(handleProjectFileChange)) + this.disposables.push(roomodesWatcher.onDidDelete(handleProjectFileDelete)) + this.disposables.push(roomodesWatcher) + } + } + + public async getCustomAgents(): Promise { + // Check if we have a valid cached result. + const now = Date.now() + + if (this.cachedAgents && now - this.cachedAt < CustomAgentsManager.cacheTTL) { + return this.cachedAgents + } + + // Get agents from settings file. + const settingsPath = await this.getCustomAgentsFilePath() + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + + // Get agents from .rooagents if it exists, otherwise check .roomodes for backward compatibility + const rooagentsPath = await this.getWorkspaceRooagents() + const roomodesPath = await this.getWorkspaceRoomodes() + + let projectAgents: AgentConfig[] = [] + if (rooagentsPath) { + projectAgents = await this.loadAgentsFromFile(rooagentsPath) + } else if (roomodesPath) { + projectAgents = await this.loadAgentsFromFile(roomodesPath) + } + + // Create maps to store agents by source. + const projectAgentMap = new Map() + const globalAgentMap = new Map() + + // Add project agents (they take precedence). + for (const agent of projectAgents) { + projectAgentMap.set(agent.slug, { ...agent, source: "project" as const }) + } + + // Add global agents. + for (const agent of settingsAgents) { + if (!projectAgentMap.has(agent.slug)) { + globalAgentMap.set(agent.slug, { ...agent, source: "global" as const }) + } + } + + // Combine agents in the correct order: project agents first, then global agents. + const mergedAgents = [ + ...projectAgents.map((agent) => ({ ...agent, source: "project" as const })), + ...settingsAgents + .filter((agent) => !projectAgentMap.has(agent.slug)) + .map((agent) => ({ ...agent, source: "global" as const })), + ] + + await this.context.globalState.update("customModes", mergedAgents) // Keep using customModes key + + this.cachedAgents = mergedAgents + this.cachedAt = now + + return mergedAgents + } + + // Backward compatibility methods + public async getCustomModes(): Promise { + return this.getCustomAgents() + } + + public async getCustomModesFilePath(): Promise { + return this.getCustomAgentsFilePath() + } + + public async updateCustomAgent(slug: string, config: AgentConfig): Promise { + try { + // Validate the agent configuration before saving + const validationResult = agentConfigSchema.safeParse(config) + if (!validationResult.success) { + const errors = validationResult.error.errors.map((e) => e.message).join(", ") + logger.error(`Invalid agent configuration for ${slug}`, { errors: validationResult.error.errors }) + throw new Error(`Invalid agent configuration: ${errors}`) + } + + const isProjectAgent = config.source === "project" + let targetPath: string + + if (isProjectAgent) { + const workspaceFolders = vscode.workspace.workspaceFolders + + if (!workspaceFolders || workspaceFolders.length === 0) { + logger.error("Failed to update project agent: No workspace folder found", { slug }) + throw new Error(t("common:customAgents.errors.noWorkspaceForProject")) + } + + const workspaceRoot = getWorkspacePath() + + // Prefer .rooagents, but check if .roomodes exists for backward compatibility + const rooagentsPath = path.join(workspaceRoot, ROOAGENTS_FILENAME) + const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME) + const rooagentsExists = await fileExistsAtPath(rooagentsPath) + const roomodesExists = await fileExistsAtPath(roomodesPath) + + if (rooagentsExists || !roomodesExists) { + // Use .rooagents (either it exists or neither exists, so create .rooagents) + targetPath = rooagentsPath + } else { + // Use existing .roomodes for backward compatibility + targetPath = roomodesPath + } + + logger.info( + `${(await fileExistsAtPath(targetPath)) ? "Updating" : "Creating"} project agent in ${path.basename(targetPath)}`, + { + slug, + workspace: workspaceRoot, + }, + ) + } else { + targetPath = await this.getCustomAgentsFilePath() + } + + await this.queueWrite(async () => { + // Ensure source is set correctly based on target file. + const agentWithSource = { + ...config, + source: isProjectAgent ? ("project" as const) : ("global" as const), + } + + await this.updateAgentsInFile(targetPath, (agents) => { + const updatedAgents = agents.filter((a) => a.slug !== slug) + updatedAgents.push(agentWithSource) + return updatedAgents + }) + + this.clearCache() + await this.refreshMergedState() + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error("Failed to update custom agent", { slug, error: errorMessage }) + vscode.window.showErrorMessage(t("common:customAgents.errors.updateFailed", { error: errorMessage })) + } + } + + // Backward compatibility method + public async updateCustomMode(slug: string, config: ModeConfig): Promise { + return this.updateCustomAgent(slug, config) + } + + private async updateAgentsInFile( + filePath: string, + operation: (agents: AgentConfig[]) => AgentConfig[], + ): Promise { + let content = "{}" + + try { + content = await fs.readFile(filePath, "utf-8") + } catch (error) { + // File might not exist yet. + const isRooagents = filePath.endsWith(ROOAGENTS_FILENAME) + if (isRooagents) { + content = yaml.stringify({ customAgents: [] }, { lineWidth: 0 }) + } else { + content = yaml.stringify({ customModes: [] }, { lineWidth: 0 }) + } + } + + let settings + + try { + settings = this.parseYamlSafely(content, filePath) + } catch (error) { + // Error already logged in parseYamlSafely + const isRooagents = filePath.endsWith(ROOAGENTS_FILENAME) + if (isRooagents) { + settings = { customAgents: [] } + } else { + settings = { customModes: [] } + } + } + + // Ensure settings is an object and has the appropriate property + if (!settings || typeof settings !== "object") { + const isRooagents = filePath.endsWith(ROOAGENTS_FILENAME) + if (isRooagents) { + settings = { customAgents: [] } + } else { + settings = { customModes: [] } + } + } + + const isRooagents = filePath.endsWith(ROOAGENTS_FILENAME) + if (isRooagents) { + if (!settings.customAgents) { + settings.customAgents = [] + } + settings.customAgents = operation(settings.customAgents) + } else { + if (!settings.customModes) { + settings.customModes = [] + } + settings.customModes = operation(settings.customModes) + } + + await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0 }), "utf-8") + } + + private async refreshMergedState(): Promise { + const settingsPath = await this.getCustomAgentsFilePath() + const rooagentsPath = await this.getWorkspaceRooagents() + const roomodesPath = await this.getWorkspaceRoomodes() + + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + + let projectAgents: AgentConfig[] = [] + if (rooagentsPath) { + projectAgents = await this.loadAgentsFromFile(rooagentsPath) + } else if (roomodesPath) { + projectAgents = await this.loadAgentsFromFile(roomodesPath) + } + + const mergedAgents = await this.mergeCustomAgents(projectAgents, settingsAgents) + + await this.context.globalState.update("customModes", mergedAgents) // Keep using customModes key + + this.clearCache() + + await this.onUpdate() + } + + public async deleteCustomAgent(slug: string, fromMarketplace = false): Promise { + try { + const settingsPath = await this.getCustomAgentsFilePath() + const rooagentsPath = await this.getWorkspaceRooagents() + const roomodesPath = await this.getWorkspaceRoomodes() + + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + + let projectAgents: AgentConfig[] = [] + let projectFilePath: string | undefined + if (rooagentsPath) { + projectAgents = await this.loadAgentsFromFile(rooagentsPath) + projectFilePath = rooagentsPath + } else if (roomodesPath) { + projectAgents = await this.loadAgentsFromFile(roomodesPath) + projectFilePath = roomodesPath + } + + // Find the agent in either file + const projectAgent = projectAgents.find((a) => a.slug === slug) + const globalAgent = settingsAgents.find((a) => a.slug === slug) + + if (!projectAgent && !globalAgent) { + throw new Error(t("common:customAgents.errors.agentNotFound")) + } + + // Determine which agent to use for rules folder path calculation + const agentToDelete = projectAgent || globalAgent + + await this.queueWrite(async () => { + // Delete from project first if it exists there + if (projectAgent && projectFilePath) { + await this.updateAgentsInFile(projectFilePath, (agents) => agents.filter((a) => a.slug !== slug)) + } + + // Delete from global settings if it exists there + if (globalAgent) { + await this.updateAgentsInFile(settingsPath, (agents) => agents.filter((a) => a.slug !== slug)) + } + + // Delete associated rules folder + if (agentToDelete) { + await this.deleteRulesFolder(slug, agentToDelete, fromMarketplace) + } + + // Clear cache when agents are deleted + this.clearCache() + await this.refreshMergedState() + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customAgents.errors.deleteFailed", { error: errorMessage })) + } + } + + // Backward compatibility method + public async deleteCustomMode(slug: string, fromMarketplace = false): Promise { + return this.deleteCustomAgent(slug, fromMarketplace) + } + + /** + * Deletes the rules folder for a specific agent + * @param slug - The agent slug + * @param agent - The agent configuration to determine the scope + */ + private async deleteRulesFolder(slug: string, agent: AgentConfig, fromMarketplace = false): Promise { + try { + // Determine the scope based on source (project or global) + const scope = agent.source || "global" + + // Determine the rules folder path + let rulesFolderPath: string + if (scope === "project") { + const workspacePath = getWorkspacePath() + if (workspacePath) { + rulesFolderPath = path.join(workspacePath, ".roo", `rules-${slug}`) + } else { + return // No workspace, can't delete project rules + } + } else { + // Global scope - use OS home directory + const homeDir = os.homedir() + rulesFolderPath = path.join(homeDir, ".roo", `rules-${slug}`) + } + + // Check if the rules folder exists and delete it + const rulesFolderExists = await fileExistsAtPath(rulesFolderPath) + if (rulesFolderExists) { + try { + await fs.rm(rulesFolderPath, { recursive: true, force: true }) + logger.info(`Deleted rules folder for agent ${slug}: ${rulesFolderPath}`) + } catch (error) { + logger.error(`Failed to delete rules folder for agent ${slug}: ${error}`) + // Notify the user about the failure + const messageKey = fromMarketplace + ? "common:marketplace.agent.rulesCleanupFailed" + : "common:customAgents.errors.rulesCleanupFailed" + vscode.window.showWarningMessage(t(messageKey, { rulesFolderPath })) + // Continue even if folder deletion fails + } + } + } catch (error) { + logger.error(`Error deleting rules folder for agent ${slug}`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + + public async resetCustomAgents(): Promise { + try { + const filePath = await this.getCustomAgentsFilePath() + await fs.writeFile(filePath, yaml.stringify({ customModes: [] }, { lineWidth: 0 })) // Keep using customModes for global + await this.context.globalState.update("customModes", []) + this.clearCache() + await this.onUpdate() + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customAgents.errors.resetFailed", { error: errorMessage })) + } + } + + // Backward compatibility method + public async resetCustomModes(): Promise { + return this.resetCustomAgents() + } + + /** + * Checks if an agent has associated rules files in the .roo/rules-{slug}/ directory + * @param slug - The agent identifier to check + * @returns True if the agent has rules files with content, false otherwise + */ + /** + * Checks if an agent has associated rules files in the .roo/rules-{slug}/ directory + * @param slug - The agent identifier to check + * @returns True if the agent has rules files with content, false otherwise + */ + public async checkRulesDirectoryHasContent(slug: string): Promise { + try { + // First, find the agent to determine its source + const allAgents = await this.getCustomAgents() + const agent = allAgents.find((a) => a.slug === slug) + + if (!agent) { + // If not in custom agents, check if it's in .rooagents or .roomodes (project-specific) + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return false + } + + const rooagentsPath = path.join(workspacePath, ROOAGENTS_FILENAME) + const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME) + + try { + let foundInProjectFile = false + + // Check .rooagents first + const rooagentsExists = await fileExistsAtPath(rooagentsPath) + if (rooagentsExists) { + const rooagentsContent = await fs.readFile(rooagentsPath, "utf-8") + const rooagentsData = yaml.parse(rooagentsContent) + const rooagentsAgents = rooagentsData?.customAgents || [] + foundInProjectFile = rooagentsAgents.find((a: any) => a.slug === slug) + } + + // Check .roomodes for backward compatibility if not found in .rooagents + if (!foundInProjectFile) { + const roomodesExists = await fileExistsAtPath(roomodesPath) + if (roomodesExists) { + const roomodesContent = await fs.readFile(roomodesPath, "utf-8") + const roomodesData = yaml.parse(roomodesContent) + const roomodesModes = roomodesData?.customModes || [] + foundInProjectFile = roomodesModes.find((m: any) => m.slug === slug) + } + } + + if (!foundInProjectFile) { + return false // Agent not found anywhere + } + } catch (error) { + return false // Cannot read project files and not in custom agents + } + } + + // Determine the correct rules directory based on agent source + let agentRulesDir: string + const isGlobalAgent = agent?.source === "global" + + if (isGlobalAgent) { + // For global agents, check in global .roo directory + const globalRooDir = getGlobalRooDirectory() + agentRulesDir = path.join(globalRooDir, `rules-${slug}`) + } else { + // For project agents, check in workspace .roo directory + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return false + } + agentRulesDir = path.join(workspacePath, ".roo", `rules-${slug}`) + } + + try { + const stats = await fs.stat(agentRulesDir) + if (!stats.isDirectory()) { + return false + } + } catch (error) { + return false + } + + // Check if directory has any content files + try { + const entries = await fs.readdir(agentRulesDir, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile()) { + // Use path.join with agentRulesDir and entry.name for compatibility + const filePath = path.join(agentRulesDir, entry.name) + const content = await fs.readFile(filePath, "utf-8") + if (content.trim()) { + return true // Found at least one file with content + } + } + } + + return false // No files with content found + } catch (error) { + return false + } + } catch (error) { + logger.error("Failed to check rules directory for agent", { + slug, + error: error instanceof Error ? error.message : String(error), + }) + return false + } + } + + /** + * Exports an agent configuration with its associated rules files into a shareable YAML format + * @param slug - The agent identifier to export + * @param customPrompts - Optional custom prompts to merge into the export + * @returns Success status with YAML content or error message + */ + public async exportAgentWithRules(slug: string, customPrompts?: PromptComponent): Promise { + try { + // Import agents from shared to check built-in agents + const { agents: builtInAgents } = await import("../../shared/modes") + + // Get all current agents + const allAgents = await this.getCustomAgents() + let agent = allAgents.find((a) => a.slug === slug) + + // If agent not found in custom agents, check if it's a built-in agent that has been customized + if (!agent) { + // Only check workspace-based agents if workspace is available + const workspacePath = getWorkspacePath() + if (workspacePath) { + const rooagentsPath = path.join(workspacePath, ROOAGENTS_FILENAME) + const roomodesPath = path.join(workspacePath, ROOMODES_FILENAME) + + try { + // Check .rooagents first + const rooagentsExists = await fileExistsAtPath(rooagentsPath) + if (rooagentsExists) { + const rooagentsContent = await fs.readFile(rooagentsPath, "utf-8") + const rooagentsData = yaml.parse(rooagentsContent) + const rooagentsAgents = rooagentsData?.customAgents || [] + agent = rooagentsAgents.find((a: any) => a.slug === slug) + } + + // Check .roomodes for backward compatibility if not found + if (!agent) { + const roomodesExists = await fileExistsAtPath(roomodesPath) + if (roomodesExists) { + const roomodesContent = await fs.readFile(roomodesPath, "utf-8") + const roomodesData = yaml.parse(roomodesContent) + const roomodesModes = roomodesData?.customModes || [] + agent = roomodesModes.find((m: any) => m.slug === slug) + } + } + } catch (error) { + // Continue to check built-in agents + } + } + + // If still not found, check if it's a built-in agent + if (!agent) { + const builtInAgent = builtInAgents.find((a) => a.slug === slug) + if (builtInAgent) { + // Use the built-in agent as the base + agent = { ...builtInAgent } + } else { + return { success: false, error: "Agent not found" } + } + } + } + + // Determine the base directory based on agent source + const isGlobalAgent = agent.source === "global" + let baseDir: string + if (isGlobalAgent) { + // For global agents, use the global .roo directory + baseDir = getGlobalRooDirectory() + } else { + // For project agents, use the workspace directory + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return { success: false, error: "No workspace found" } + } + baseDir = workspacePath + } + + // Check for .roo/rules-{slug}/ directory (or rules-{slug}/ for global) + const agentRulesDir = isGlobalAgent + ? path.join(baseDir, `rules-${slug}`) + : path.join(baseDir, ".roo", `rules-${slug}`) + + let rulesFiles: RuleFile[] = [] + try { + const stats = await fs.stat(agentRulesDir) + if (stats.isDirectory()) { + // Extract content specific to this agent by looking for the agent-specific rules + const entries = await fs.readdir(agentRulesDir, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.isFile()) { + // Use path.join with agentRulesDir and entry.name for compatibility + const filePath = path.join(agentRulesDir, entry.name) + const content = await fs.readFile(filePath, "utf-8") + if (content.trim()) { + // Calculate relative path based on agent source + const relativePath = isGlobalAgent + ? path.relative(baseDir, filePath) + : path.relative(path.join(baseDir, ".roo"), filePath) + // Normalize path to use forward slashes for cross-platform compatibility + const normalizedRelativePath = relativePath.replace(/\\/g, "/") + rulesFiles.push({ relativePath: normalizedRelativePath, content: content.trim() }) + } + } + } + } + } catch (error) { + // Directory doesn't exist, which is fine - agent might not have rules + } + + // Create an export agent with rules files preserved + const exportAgent: ExportedAgentConfig = { + ...agent, + // Remove source property for export + source: "project" as const, + } + + // Merge custom prompts if provided + if (customPrompts) { + if (customPrompts.roleDefinition) exportAgent.roleDefinition = customPrompts.roleDefinition + if (customPrompts.description) exportAgent.description = customPrompts.description + if (customPrompts.whenToUse) exportAgent.whenToUse = customPrompts.whenToUse + if (customPrompts.customInstructions) exportAgent.customInstructions = customPrompts.customInstructions + } + + // Add rules files if any exist + if (rulesFiles.length > 0) { + exportAgent.rulesFiles = rulesFiles + } + + // Generate YAML + const exportData = { + customAgents: [exportAgent], + } + + const yamlContent = yaml.stringify(exportData) + + return { success: true, yaml: yamlContent } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error("Failed to export agent with rules", { slug, error: errorMessage }) + return { success: false, error: errorMessage } + } + } + + // Backward compatibility method + public async exportModeWithRules(slug: string, customPrompts?: PromptComponent): Promise { + return this.exportAgentWithRules(slug, customPrompts) + } + + /** + * Helper method to import rules files for an agent + * @param importAgent - The agent being imported + * @param rulesFiles - The rules files to import + * @param source - The import source ("global" or "project") + */ + private async importRulesFiles( + importAgent: ExportedAgentConfig, + rulesFiles: RuleFile[], + source: "global" | "project", + ): Promise { + // Determine base directory and rules folder path based on source + let baseDir: string + let rulesFolderPath: string + + if (source === "global") { + baseDir = getGlobalRooDirectory() + rulesFolderPath = path.join(baseDir, `rules-${importAgent.slug}`) + } else { + const workspacePath = getWorkspacePath() + baseDir = path.join(workspacePath, ".roo") + rulesFolderPath = path.join(baseDir, `rules-${importAgent.slug}`) + } + + // Always remove the existing rules folder for this agent if it exists + // This ensures that if the imported agent has no rules, the folder is cleaned up + try { + await fs.rm(rulesFolderPath, { recursive: true, force: true }) + logger.info(`Removed existing ${source} rules folder for agent ${importAgent.slug}`) + } catch (error) { + // It's okay if the folder doesn't exist + logger.debug(`No existing ${source} rules folder to remove for agent ${importAgent.slug}`) + } + + // Only proceed with file creation if there are rules files to import + if (!rulesFiles || !Array.isArray(rulesFiles) || rulesFiles.length === 0) { + return + } + + // Import the new rules files with path validation + for (const ruleFile of rulesFiles) { + if (ruleFile.relativePath && ruleFile.content) { + // Validate the relative path to prevent path traversal attacks + const normalizedRelativePath = path.normalize(ruleFile.relativePath) + + // Ensure the path doesn't contain traversal sequences + if (normalizedRelativePath.includes("..") || path.isAbsolute(normalizedRelativePath)) { + logger.error(`Invalid file path detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + const targetPath = path.join(baseDir, normalizedRelativePath) + const normalizedTargetPath = path.normalize(targetPath) + const expectedBasePath = path.normalize(baseDir) + + // Ensure the resolved path stays within the base directory + if (!normalizedTargetPath.startsWith(expectedBasePath)) { + logger.error(`Path traversal attempt detected: ${ruleFile.relativePath}`) + continue // Skip this file but continue with others + } + + // Ensure directory exists + const targetDir = path.dirname(targetPath) + await fs.mkdir(targetDir, { recursive: true }) + + // Write the file + await fs.writeFile(targetPath, ruleFile.content, "utf-8") + } + } + } + + /** + * Imports agents from YAML content, including their associated rules files + * @param yamlContent - The YAML content containing agent configurations + * @param source - Target level for import: "global" (all projects) or "project" (current workspace only) + * @returns Success status with optional error message + */ + public async importAgentWithRules( + yamlContent: string, + source: "global" | "project" = "project", + ): Promise { + try { + // Parse the YAML content with proper type validation + let importData: ImportData + try { + const parsed = yaml.parse(yamlContent) + + // Handle both new format (customAgents) and legacy format (customModes) + if (parsed?.customAgents && Array.isArray(parsed.customAgents) && parsed.customAgents.length > 0) { + importData = { customAgents: parsed.customAgents } + } else if (parsed?.customModes && Array.isArray(parsed.customModes) && parsed.customModes.length > 0) { + // Convert legacy customModes to customAgents + importData = { customAgents: parsed.customModes } + } else { + return { + success: false, + error: "Invalid import format: Expected 'customAgents' or 'customModes' array in YAML", + } + } + } catch (parseError) { + return { + success: false, + error: `Invalid YAML format: ${parseError instanceof Error ? parseError.message : "Failed to parse YAML"}`, + } + } + + // Check workspace availability early if importing at project level + if (source === "project") { + const workspacePath = getWorkspacePath() + if (!workspacePath) { + return { success: false, error: "No workspace found" } + } + } + + // Process each agent in the import + for (const importAgent of importData.customAgents!) { + const { rulesFiles, ...agentConfig } = importAgent + + // Validate the agent configuration + const validationResult = agentConfigSchema.safeParse(agentConfig) + if (!validationResult.success) { + logger.error(`Invalid agent configuration for ${agentConfig.slug}`, { + errors: validationResult.error.errors, + }) + return { + success: false, + error: `Invalid agent configuration for ${agentConfig.slug}: ${validationResult.error.errors.map((e) => e.message).join(", ")}`, + } + } + + // Check for existing agent conflicts + const existingAgents = await this.getCustomAgents() + const existingAgent = existingAgents.find((a) => a.slug === importAgent.slug) + if (existingAgent) { + logger.info(`Overwriting existing agent: ${importAgent.slug}`) + } + + // Import the agent configuration with the specified source + await this.updateCustomAgent(importAgent.slug, { + ...agentConfig, + source: source, // Use the provided source parameter + }) + + // Import rules files (this also handles cleanup of existing rules folders) + await this.importRulesFiles(importAgent, rulesFiles || [], source) + } + + // Refresh the agents after import + await this.refreshMergedState() + + return { success: true } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error("Failed to import agent with rules", { error: errorMessage }) + return { success: false, error: errorMessage } + } + } + + // Backward compatibility method + public async importModeWithRules( + yamlContent: string, + source: "global" | "project" = "project", + ): Promise { + return this.importAgentWithRules(yamlContent, source) + } + + private clearCache(): void { + this.cachedAgents = null + this.cachedAt = 0 + } + + // Additional backward compatibility properties and methods to match CustomModesManager interface + get cachedModes(): AgentConfig[] { + return this.cachedAgents || [] + } + + async loadModesFromFile(): Promise { + const settingsPath = await this.getCustomAgentsFilePath() + return this.loadAgentsFromFile(settingsPath) + } + + async mergeCustomModes(newModes: AgentConfig[]): Promise { + // This method updates the global state with merged modes + const settingsPath = await this.getCustomAgentsFilePath() + const settingsAgents = await this.loadAgentsFromFile(settingsPath) + const mergedAgents = await this.mergeCustomAgents(newModes, settingsAgents) + await this.context.globalState.update("customModes", mergedAgents) + this.clearCache() + await this.onUpdate() + } + + watchCustomModesFiles(): void { + // This method is already called in constructor, so just return + return + } + + async updateModesInFile(modes: AgentConfig[]): Promise { + const settingsPath = await this.getCustomAgentsFilePath() + await this.queueWrite(async () => { + await this.updateAgentsInFile(settingsPath, () => modes) + this.clearCache() + await this.refreshMergedState() + }) + } + + dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose() + } + + this.disposables = [] + } +} diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index c3d6f9c215..d6717a764d 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -12,12 +12,13 @@ import { TelemetryService } from "@roo-code/telemetry" import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager" import { ContextProxy } from "./ContextProxy" import { CustomModesManager } from "./CustomModesManager" +import { CustomAgentsManager } from "./CustomAgentsManager" import { t } from "../../i18n" export type ImportOptions = { providerSettingsManager: ProviderSettingsManager contextProxy: ContextProxy - customModesManager: CustomModesManager + customModesManager: CustomModesManager | CustomAgentsManager } type ExportOptions = { @@ -65,7 +66,14 @@ export async function importSettingsFromPath( } await Promise.all( - (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), + (globalSettings.customModes ?? []).map((mode) => { + // Support both CustomModesManager and CustomAgentsManager + if ("updateCustomAgent" in customModesManager) { + return customModesManager.updateCustomAgent(mode.slug, mode) + } else { + return customModesManager.updateCustomMode(mode.slug, mode) + } + }), ) // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1218999a9a..da68b0ce5c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -59,7 +59,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" -import { CustomModesManager } from "../config/CustomModesManager" +import { CustomAgentsManager } from "../config/CustomAgentsManager" import { buildApiHandler } from "../../api" import { Task, TaskOptions } from "../task/Task" import { getNonce } from "./getNonce" @@ -114,7 +114,9 @@ export class ClineProvider public settingsImportedAt?: number public readonly latestAnnouncementId = "jul-26-2025-3-24-0" // Update for v3.24.0 announcement public readonly providerSettingsManager: ProviderSettingsManager - public readonly customModesManager: CustomModesManager + public readonly customAgentsManager: CustomAgentsManager + // Backward compatibility alias + public readonly customModesManager: CustomAgentsManager constructor( readonly context: vscode.ExtensionContext, @@ -144,9 +146,11 @@ export class ClineProvider this.providerSettingsManager = new ProviderSettingsManager(this.context) - this.customModesManager = new CustomModesManager(this.context, async () => { + this.customAgentsManager = new CustomAgentsManager(this.context, async () => { await this.postStateToWebview() }) + // Backward compatibility alias + this.customModesManager = this.customAgentsManager // Initialize MCP Hub through the singleton manager McpServerManager.getInstance(this.context, this) @@ -158,7 +162,7 @@ export class ClineProvider this.log(`Failed to initialize MCP Hub: ${error}`) }) - this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager) + this.marketplaceManager = new MarketplaceManager(this.context, this.customAgentsManager) } // Adds a new Cline instance to clineStack, marking the start of a new task. @@ -174,7 +178,7 @@ export class ClineProvider const state = await this.getState() if (!state || typeof state.mode !== "string") { - throw new Error(t("common:errors.retrieve_current_mode")) + throw new Error(t("common:errors.retrieve_current_agent")) } } @@ -281,7 +285,7 @@ export class ClineProvider await this.mcpHub?.unregisterClient() this.mcpHub = undefined this.marketplaceManager?.cleanup() - this.customModesManager?.dispose() + this.customAgentsManager?.dispose() this.log("Disposed all disposables") ClineProvider.activeInstances.delete(this) @@ -581,7 +585,7 @@ export class ClineProvider // If the history item has a saved mode, restore it and its associated API configuration if (historyItem.mode) { // Validate that the mode still exists - const customModes = await this.customModesManager.getCustomModes() + const customModes = await this.customAgentsManager.getCustomModes() const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined if (!modeExists) { @@ -1646,7 +1650,7 @@ export class ClineProvider async getState() { const stateValues = this.contextProxy.getValues() - const customModes = await this.customModesManager.getCustomModes() + const customModes = await this.customAgentsManager.getCustomModes() // Determine apiProvider with the same logic as before. const apiProvider: ProviderName = stateValues.apiProvider ? stateValues.apiProvider : "anthropic" @@ -1871,7 +1875,7 @@ export class ClineProvider await this.contextProxy.resetAllState() await this.providerSettingsManager.resetAllConfigs() - await this.customModesManager.resetCustomModes() + await this.customAgentsManager.resetCustomModes() await this.removeClineFromStack() await this.postStateToWebview() await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 763e118125..8e8af36491 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -211,7 +211,7 @@ export const webviewMessageHandler = async ( switch (message.type) { case "webviewDidLaunch": // Load custom modes first - const customModes = await provider.customModesManager.getCustomModes() + const customModes = await provider.customAgentsManager.getCustomModes() await updateGlobalState("customModes", customModes) provider.postStateToWebview() @@ -491,7 +491,7 @@ export const webviewMessageHandler = async ( await importSettingsWithFeedback({ providerSettingsManager: provider.providerSettingsManager, contextProxy: provider.contextProxy, - customModesManager: provider.customModesManager, + customModesManager: provider.customAgentsManager, provider: provider, }) @@ -772,7 +772,7 @@ export const webviewMessageHandler = async ( break } case "openCustomModesSettings": { - const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() + const customModesFilePath = await provider.customAgentsManager.getCustomModesFilePath() if (customModesFilePath) { openFile(customModesFilePath) @@ -1632,12 +1632,12 @@ export const webviewMessageHandler = async ( case "updateCustomMode": if (message.modeConfig) { // Check if this is a new mode or an update to an existing mode - const existingModes = await provider.customModesManager.getCustomModes() + const existingModes = await provider.customAgentsManager.getCustomModes() const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug) - await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) + await provider.customAgentsManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) // Update state after saving the mode - const customModes = await provider.customModesManager.getCustomModes() + const customModes = await provider.customAgentsManager.getCustomModes() await updateGlobalState("customModes", customModes) await updateGlobalState("mode", message.modeConfig.slug) await provider.postStateToWebview() @@ -1671,7 +1671,7 @@ export const webviewMessageHandler = async ( case "deleteCustomMode": if (message.slug) { // Get the mode details to determine source and rules folder path - const customModes = await provider.customModesManager.getCustomModes() + const customModes = await provider.customAgentsManager.getCustomModes() const modeToDelete = customModes.find((mode) => mode.slug === message.slug) if (!modeToDelete) { @@ -1710,7 +1710,7 @@ export const webviewMessageHandler = async ( } // Delete the mode - await provider.customModesManager.deleteCustomMode(message.slug) + await provider.customAgentsManager.deleteCustomMode(message.slug) // Delete the rules folder if it exists if (rulesFolderExists) { @@ -1743,7 +1743,7 @@ export const webviewMessageHandler = async ( const customPrompt = customModePrompts[message.slug] // Export the mode with any customizations merged directly - const result = await provider.customModesManager.exportModeWithRules(message.slug, customPrompt) + const result = await provider.customAgentsManager.exportModeWithRules(message.slug, customPrompt) if (result.success && result.yaml) { // Get last used directory for export @@ -1790,7 +1790,9 @@ export const webviewMessageHandler = async ( }) // Show info message - vscode.window.showInformationMessage(t("common:info.mode_exported", { mode: message.slug })) + vscode.window.showInformationMessage( + t("common:info.agent_exported", { agent: message.slug }), + ) } else { // User cancelled the save dialog provider.postMessageToWebview({ @@ -1879,7 +1881,7 @@ export const webviewMessageHandler = async ( }) // Show success message - vscode.window.showInformationMessage(t("common:info.mode_imported")) + vscode.window.showInformationMessage(t("common:info.agent_imported")) } else { // Send error message to webview provider.postMessageToWebview({ @@ -1916,7 +1918,7 @@ export const webviewMessageHandler = async ( break case "checkRulesDirectory": if (message.slug) { - const hasContent = await provider.customModesManager.checkRulesDirectoryHasContent(message.slug) + const hasContent = await provider.customAgentsManager.checkRulesDirectoryHasContent(message.slug) provider.postMessageToWebview({ type: "checkRulesDirectoryResult", diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 0fba764080..c96625152f 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -21,7 +21,7 @@ "confirmation": { "reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.", "delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?", - "delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Esteu segur que voleu suprimir aquest Agent {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Format d'URI de dades no vàlid", @@ -47,7 +47,7 @@ "list_api_config": "Ha fallat l'obtenció de la llista de configuracions de l'API", "update_server_timeout": "Ha fallat l'actualització del temps d'espera del servidor", "hmr_not_running": "El servidor de desenvolupament local no està executant-se, l'HMR no funcionarà. Si us plau, executa 'npm run dev' abans de llançar l'extensió per habilitar l'HMR.", - "retrieve_current_mode": "Error en recuperar el mode actual de l'estat.", + "retrieve_current_mode": "Error en recuperar el Agent actual de l'estat.", "failed_delete_repo": "Ha fallat l'eliminació del repositori o branca associada: {{error}}", "failed_remove_directory": "Ha fallat l'eliminació del directori de tasques: {{error}}", "custom_storage_path_unusable": "La ruta d'emmagatzematge personalitzada \"{{path}}\" no és utilitzable, s'utilitzarà la ruta predeterminada", @@ -93,7 +93,7 @@ "generate_complete_prompt": "Error de finalització de Gemini: {{error}}", "sources": "Fonts:" }, - "mode_import_failed": "Ha fallat la importació del mode: {{error}}" + "agent_import_failed": "Ha fallat la importació del Agent: {{error}}" }, "warnings": { "no_terminal_content": "No s'ha seleccionat contingut de terminal", @@ -113,8 +113,8 @@ "image_saved": "Imatge desada a {{path}}", "organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!", "public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!", - "mode_exported": "Mode '{{mode}}' exportat correctament", - "mode_imported": "Mode importat correctament" + "agent_exported": "Agent '{{Agent}}' exportat correctament", + "agent_imported": "Agent importat correctament" }, "answers": { "yes": "Sí", @@ -150,17 +150,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML no vàlid al fitxer .roomodes a la línia {{line}}. Comprova:\n• Indentació correcta (utilitza espais, no tabuladors)\n• Cometes i claudàtors coincidents\n• Sintaxi YAML vàlida", - "schemaValidationError": "Format de modes personalitzats no vàlid a .roomodes:\n{{issues}}", - "invalidFormat": "Format de modes personalitzats no vàlid. Assegura't que la teva configuració segueix el format YAML correcte.", - "updateFailed": "Error en actualitzar el mode personalitzat: {{error}}", - "deleteFailed": "Error en eliminar el mode personalitzat: {{error}}", - "resetFailed": "Error en restablir els modes personalitzats: {{error}}", - "modeNotFound": "Error d'escriptura: Mode no trobat", - "noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte", - "rulesCleanupFailed": "El mode s'ha suprimit correctament, però no s'ha pogut suprimir la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis de suprimir manualment." + "yamlParseError": "YAML no vàlid al fitxer .rooagents a la línia {{line}}. Comprova:\n• Indentació correcta (utilitza espais, no tabuladors)\n• Cometes i claudàtors coincidents\n• Sintaxi YAML vàlida", + "schemaValidationError": "Format de Agents personalitzats no vàlid a .rooagents:\n{{issues}}", + "invalidFormat": "Format de Agents personalitzats no vàlid. Assegura't que la teva configuració segueix el format YAML correcte.", + "updateFailed": "Error en actualitzar el Agent personalitzat: {{error}}", + "deleteFailed": "Error en eliminar el Agent personalitzat: {{error}}", + "resetFailed": "Error en restablir els Agents personalitzats: {{error}}", + "modeNotFound": "Error d'escriptura: Agent no trobat", + "noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al Agent específic del projecte", + "rulesCleanupFailed": "El Agent s'ha suprimit correctament, però no s'ha pogut suprimir la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis de suprimir manualment." }, "scope": { "project": "projecte", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "El mode s'ha eliminat correctament, però no s'ha pogut eliminar la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis d'eliminar manualment." + "Agent": { + "rulesCleanupFailed": "El Agent s'ha eliminat correctament, però no s'ha pogut eliminar la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis d'eliminar manualment." } }, "mdm": { @@ -180,10 +180,10 @@ } }, "prompts": { - "deleteMode": { - "title": "Suprimeix el mode personalitzat", - "description": "Esteu segur que voleu suprimir aquest mode {{scope}}? Això també suprimirà la carpeta de regles associada a: {{rulesFolderPath}}", - "descriptionNoRules": "Esteu segur que voleu suprimir aquest mode personalitzat?", + "deleteAgent": { + "title": "Suprimeix el Agent personalitzat", + "description": "Esteu segur que voleu suprimir aquest Agent {{scope}}? Això també suprimirà la carpeta de regles associada a: {{rulesFolderPath}}", + "descriptionNoRules": "Esteu segur que voleu suprimir aquest Agent personalitzat?", "confirm": "Suprimeix" } }, diff --git a/src/i18n/locales/ca/marketplace.json b/src/i18n/locales/ca/marketplace.json index 6c64374447..09f1e42173 100644 --- a/src/i18n/locales/ca/marketplace.json +++ b/src/i18n/locales/ca/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modes", + "Agents": "Agents", "mcps": "Servidors MCP", "match": "coincidència" }, "item-card": { - "type-mode": "Mode", + "type-Agent": "Agent", "type-mcp": "Servidor MCP", "type-other": "Altre", "by-author": "per {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Tipus", "all": "Tots els tipus", - "mode": "Mode", + "Agent": "Agent", "mcpServer": "Servidor MCP" }, "sort": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 1c60189b2f..10f4f3a908 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.", "delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?", - "delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Bist du sicher, dass du diesen {scope}-Agent löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Ungültiges Daten-URI-Format", @@ -43,7 +43,7 @@ "list_api_config": "Fehler beim Abrufen der API-Konfigurationsliste", "update_server_timeout": "Fehler beim Aktualisieren des Server-Timeouts", "hmr_not_running": "Der lokale Entwicklungsserver läuft nicht, HMR wird nicht funktionieren. Bitte führen Sie 'npm run dev' vor dem Start der Erweiterung aus, um HMR zu aktivieren.", - "retrieve_current_mode": "Fehler beim Abrufen des aktuellen Modus aus dem Zustand.", + "retrieve_current_agent": "Fehler beim Abrufen des aktuellen Agents aus dem Zustand.", "failed_delete_repo": "Fehler beim Löschen des zugehörigen Shadow-Repositorys oder -Zweigs: {{error}}", "failed_remove_directory": "Fehler beim Entfernen des Aufgabenverzeichnisses: {{error}}", "custom_storage_path_unusable": "Benutzerdefinierter Speicherpfad \"{{path}}\" ist nicht verwendbar, Standardpfad wird verwendet", @@ -69,7 +69,7 @@ "share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.", "share_not_enabled": "Aufgabenfreigabe ist für diese Organisation nicht aktiviert.", "share_task_not_found": "Aufgabe nicht gefunden oder Zugriff verweigert.", - "mode_import_failed": "Fehler beim Importieren des Modus: {{error}}", + "agent_import_failed": "Fehler beim Importieren des Agents: {{error}}", "delete_rules_folder_failed": "Fehler beim Löschen des Regelordners: {{rulesFolderPath}}. Fehler: {{error}}", "command_not_found": "Befehl '{{name}}' nicht gefunden", "open_command_file": "Fehler beim Öffnen der Befehlsdatei", @@ -109,8 +109,8 @@ "image_saved": "Bild gespeichert unter {{path}}", "organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!", "public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!", - "mode_exported": "Modus '{{mode}}' erfolgreich exportiert", - "mode_imported": "Modus erfolgreich importiert" + "agent_exported": "Agent '{{agent}}' erfolgreich exportiert", + "agent_imported": "Agent erfolgreich importiert" }, "answers": { "yes": "Ja", @@ -150,17 +150,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "Ungültiges YAML in .roomodes-Datei in Zeile {{line}}. Bitte überprüfe:\n• Korrekte Einrückung (verwende Leerzeichen, keine Tabs)\n• Passende Anführungszeichen und Klammern\n• Gültige YAML-Syntax", - "schemaValidationError": "Ungültiges Format für benutzerdefinierte Modi in .roomodes:\n{{issues}}", - "invalidFormat": "Ungültiges Format für benutzerdefinierte Modi. Bitte stelle sicher, dass deine Einstellungen dem korrekten YAML-Format folgen.", - "updateFailed": "Fehler beim Aktualisieren des benutzerdefinierten Modus: {{error}}", - "deleteFailed": "Fehler beim Löschen des benutzerdefinierten Modus: {{error}}", - "resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Modi: {{error}}", - "modeNotFound": "Schreibfehler: Modus nicht gefunden", - "noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden", - "rulesCleanupFailed": "Der Modus wurde erfolgreich gelöscht, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." + "yamlParseError": "Ungültiges YAML in .rooagents-Datei in Zeile {{line}}. Bitte überprüfe:\n• Korrekte Einrückung (verwende Leerzeichen, keine Tabs)\n• Passende Anführungszeichen und Klammern\n• Gültige YAML-Syntax", + "schemaValidationError": "Ungültiges Format für benutzerdefinierte Agenten in .rooagents:\n{{issues}}", + "invalidFormat": "Ungültiges Format für benutzerdefinierte Agenten. Bitte stelle sicher, dass deine Einstellungen dem korrekten YAML-Format folgen.", + "updateFailed": "Fehler beim Aktualisieren des benutzerdefinierten Agents: {{error}}", + "deleteFailed": "Fehler beim Löschen des benutzerdefinierten Agents: {{error}}", + "resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Agenten: {{error}}", + "agentNotFound": "Schreibfehler: Agent nicht gefunden", + "noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Agent gefunden", + "rulesCleanupFailed": "Der Agent wurde erfolgreich gelöscht, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." }, "scope": { "project": "projekt", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "Der Modus wurde erfolgreich entfernt, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." + "agent": { + "rulesCleanupFailed": "Der Agent wurde erfolgreich entfernt, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen." } }, "mdm": { diff --git a/src/i18n/locales/de/marketplace.json b/src/i18n/locales/de/marketplace.json index 2981441cf0..e6b7885ed5 100644 --- a/src/i18n/locales/de/marketplace.json +++ b/src/i18n/locales/de/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modi", + "agents": "Agenten", "mcps": "MCP-Server", "match": "Übereinstimmung" }, "item-card": { - "type-mode": "Modus", + "type-agent": "Agent", "type-mcp": "MCP-Server", "type-other": "Andere", "by-author": "von {{author}}", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 114e129f45..70627562be 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.", "delete_config_profile": "Are you sure you want to delete this configuration profile?", - "delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Are you sure you want to delete this {scope} agent?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Invalid data URI format", @@ -43,7 +43,7 @@ "list_api_config": "Failed to get list api configuration", "update_server_timeout": "Failed to update server timeout", "hmr_not_running": "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.", - "retrieve_current_mode": "Error: failed to retrieve current mode from state.", + "retrieve_current_agent": "Error: failed to retrieve current agent from state.", "failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}", "failed_remove_directory": "Failed to remove task directory: {{error}}", "custom_storage_path_unusable": "Custom storage path \"{{path}}\" is unusable, will use default path", @@ -69,7 +69,7 @@ "share_auth_required": "Authentication required. Please sign in to share tasks.", "share_not_enabled": "Task sharing is not enabled for this organization.", "share_task_not_found": "Task not found or access denied.", - "mode_import_failed": "Failed to import mode: {{error}}", + "agent_import_failed": "Failed to import agent: {{error}}", "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", "command_not_found": "Command '{{name}}' not found", "open_command_file": "Failed to open command file", @@ -109,8 +109,8 @@ "public_share_link_copied": "Public share link copied to clipboard!", "image_copied_to_clipboard": "Image data URI copied to clipboard", "image_saved": "Image saved to {{path}}", - "mode_exported": "Mode '{{mode}}' exported successfully", - "mode_imported": "Mode imported successfully" + "agent_exported": "Agent '{{agent}}' exported successfully", + "agent_imported": "Agent imported successfully" }, "answers": { "yes": "Yes", @@ -139,17 +139,17 @@ "task_prompt": "What should Roo do?", "task_placeholder": "Type your task here" }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "Invalid YAML in .roomodes file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax", - "schemaValidationError": "Invalid custom modes format in .roomodes:\n{{issues}}", - "invalidFormat": "Invalid custom modes format. Please ensure your settings follow the correct YAML format.", - "updateFailed": "Failed to update custom mode: {{error}}", - "deleteFailed": "Failed to delete custom mode: {{error}}", - "resetFailed": "Failed to reset custom modes: {{error}}", - "modeNotFound": "Write error: Mode not found", - "noWorkspaceForProject": "No workspace folder found for project-specific mode", - "rulesCleanupFailed": "Mode deleted successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." + "yamlParseError": "Invalid YAML in .rooagents file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax", + "schemaValidationError": "Invalid custom agents format in .rooagents:\n{{issues}}", + "invalidFormat": "Invalid custom agents format. Please ensure your settings follow the correct YAML format.", + "updateFailed": "Failed to update custom agent: {{error}}", + "deleteFailed": "Failed to delete custom agent: {{error}}", + "resetFailed": "Failed to reset custom agents: {{error}}", + "agentNotFound": "Write error: Agent not found", + "noWorkspaceForProject": "No workspace folder found for project-specific agent", + "rulesCleanupFailed": "Agent deleted successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." }, "scope": { "project": "project", @@ -157,8 +157,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "Mode removed successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." + "agent": { + "rulesCleanupFailed": "Agent removed successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually." } }, "mdm": { @@ -169,10 +169,10 @@ } }, "prompts": { - "deleteMode": { - "title": "Delete Custom Mode", - "description": "Are you sure you want to delete this {{scope}} mode? This will also delete the associated rules folder at: {{rulesFolderPath}}", - "descriptionNoRules": "Are you sure you want to delete this custom mode?", + "deleteAgent": { + "title": "Delete Custom Agent", + "description": "Are you sure you want to delete this {{scope}} agent? This will also delete the associated rules folder at: {{rulesFolderPath}}", + "descriptionNoRules": "Are you sure you want to delete this custom agent?", "confirm": "Delete" } }, diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 62ab4dcb6e..bff194cc27 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.", "delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?", - "delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "¿Estás seguro de que quieres eliminar este agente {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Formato de URI de datos no válido", @@ -43,7 +43,7 @@ "list_api_config": "Error al obtener la lista de configuraciones de API", "update_server_timeout": "Error al actualizar el tiempo de espera del servidor", "hmr_not_running": "El servidor de desarrollo local no está en ejecución, HMR no funcionará. Por favor, ejecuta 'npm run dev' antes de lanzar la extensión para habilitar HMR.", - "retrieve_current_mode": "Error al recuperar el modo actual del estado.", + "retrieve_current_agent": "Error al recuperar el agente actual del estado.", "failed_delete_repo": "Error al eliminar el repositorio o rama asociada: {{error}}", "failed_remove_directory": "Error al eliminar el directorio de tareas: {{error}}", "custom_storage_path_unusable": "La ruta de almacenamiento personalizada \"{{path}}\" no es utilizable, se usará la ruta predeterminada", @@ -69,7 +69,7 @@ "share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.", "share_not_enabled": "La compartición de tareas no está habilitada para esta organización.", "share_task_not_found": "Tarea no encontrada o acceso denegado.", - "mode_import_failed": "Error al importar el modo: {{error}}", + "agent_import_failed": "Error al importar el agente: {{error}}", "delete_rules_folder_failed": "Error al eliminar la carpeta de reglas: {{rulesFolderPath}}. Error: {{error}}", "command_not_found": "Comando '{{name}}' no encontrado", "open_command_file": "Error al abrir el archivo de comandos", @@ -109,8 +109,8 @@ "image_saved": "Imagen guardada en {{path}}", "organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!", "public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!", - "mode_exported": "Modo '{{mode}}' exportado correctamente", - "mode_imported": "Modo importado correctamente" + "agent_exported": "Agente '{{agent}}' exportado correctamente", + "agent_imported": "Agente importado correctamente" }, "answers": { "yes": "Sí", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manually." + "agent": { + "rulesCleanupFailed": "El agente se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manualmente." } }, "mdm": { @@ -180,10 +180,10 @@ } }, "prompts": { - "deleteMode": { - "title": "Eliminar modo personalizado", - "description": "¿Estás seguro de que quieres eliminar este modo {{scope}}? Esto también eliminará la carpeta de reglas asociada en: {{rulesFolderPath}}", - "descriptionNoRules": "¿Estás seguro de que quieres eliminar este modo personalizado?", + "deleteAgent": { + "title": "Eliminar agente personalizado", + "description": "¿Estás seguro de que quieres eliminar este agente {{scope}}? Esto también eliminará la carpeta de reglas asociada en: {{rulesFolderPath}}", + "descriptionNoRules": "¿Estás seguro de que quieres eliminar este agente personalizado?", "confirm": "Eliminar" } }, diff --git a/src/i18n/locales/es/marketplace.json b/src/i18n/locales/es/marketplace.json index e12e1d1dc1..e02ab2c4e9 100644 --- a/src/i18n/locales/es/marketplace.json +++ b/src/i18n/locales/es/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modos", + "agents": "Agentes", "mcps": "Servidores MCP", "match": "coincidencia" }, "item-card": { - "type-mode": "Modo", + "type-agent": "Agente", "type-mcp": "Servidor MCP", "type-other": "Otro", "by-author": "por {{author}}", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index aae4d5d7b1..da6c6dba3f 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Êtes-vous sûr de vouloir réinitialiser le global state et le stockage de secrets de l'extension ? Cette action est irréversible.", "delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?", - "delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Êtes-vous sûr de vouloir supprimer cet agent {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Format d'URI de données invalide", @@ -43,7 +43,7 @@ "list_api_config": "Erreur lors de l'obtention de la liste des configurations API", "update_server_timeout": "Erreur lors de la mise à jour du délai d'attente du serveur", "hmr_not_running": "Le serveur de développement local n'est pas en cours d'exécution, HMR ne fonctionnera pas. Veuillez exécuter 'npm run dev' avant de lancer l'extension pour activer l'HMR.", - "retrieve_current_mode": "Erreur lors de la récupération du mode actuel à partir du state.", + "retrieve_current_agent": "Erreur lors de la récupération de l'agent actuel à partir du state.", "failed_delete_repo": "Échec de la suppression du repo fantôme ou de la branche associée : {{error}}", "failed_remove_directory": "Échec de la suppression du répertoire de tâches : {{error}}", "custom_storage_path_unusable": "Le chemin de stockage personnalisé \"{{path}}\" est inutilisable, le chemin par défaut sera utilisé", @@ -69,7 +69,7 @@ "share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.", "share_not_enabled": "Le partage de tâches n'est pas activé pour cette organisation.", "share_task_not_found": "Tâche non trouvée ou accès refusé.", - "mode_import_failed": "Échec de l'importation du mode : {{error}}", + "agent_import_failed": "Échec de l'importation de l'agent : {{error}}", "delete_rules_folder_failed": "Échec de la suppression du dossier de règles : {{rulesFolderPath}}. Erreur : {{error}}", "command_not_found": "Commande '{{name}}' introuvable", "open_command_file": "Échec de l'ouverture du fichier de commande", @@ -109,8 +109,8 @@ "image_saved": "Image enregistrée dans {{path}}", "organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !", "public_share_link_copied": "Lien de partage public copié dans le presse-papiers !", - "mode_exported": "Mode '{{mode}}' exporté avec succès", - "mode_imported": "Mode importé avec succès" + "agent_exported": "Agent '{{agent}}' exporté avec succès", + "agent_imported": "Agent importé avec succès" }, "answers": { "yes": "Oui", @@ -150,17 +150,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML invalide dans le fichier .roomodes à la ligne {{line}}. Vérifie :\n• L'indentation correcte (utilise des espaces, pas de tabulations)\n• Les guillemets et crochets correspondants\n• La syntaxe YAML valide", - "schemaValidationError": "Format invalide des modes personnalisés dans .roomodes :\n{{issues}}", - "invalidFormat": "Format invalide des modes personnalisés. Assure-toi que tes paramètres suivent le format YAML correct.", - "updateFailed": "Échec de la mise à jour du mode personnalisé : {{error}}", - "deleteFailed": "Échec de la suppression du mode personnalisé : {{error}}", - "resetFailed": "Échec de la réinitialisation des modes personnalisés : {{error}}", - "modeNotFound": "Erreur d'écriture : Mode non trouvé", - "noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour le mode spécifique au projet", - "rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." + "yamlParseError": "YAML invalide dans le fichier .rooagents à la ligne {{line}}. Vérifie :\n• L'indentation correcte (utilise des espaces, pas de tabulations)\n• Les guillemets et crochets correspondants\n• La syntaxe YAML valide", + "schemaValidationError": "Format invalide des agents personnalisés dans .rooagents :\n{{issues}}", + "invalidFormat": "Format invalide des agents personnalisés. Assure-toi que tes paramètres suivent le format YAML correct.", + "updateFailed": "Échec de la mise à jour de l'agent personnalisé : {{error}}", + "deleteFailed": "Échec de la suppression de l'agent personnalisé : {{error}}", + "resetFailed": "Échec de la réinitialisation des agents personnalisés : {{error}}", + "agentNotFound": "Erreur d'écriture : Agent non trouvé", + "noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour l'agent spécifique au projet", + "rulesCleanupFailed": "L'agent a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." }, "scope": { "project": "projet", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." + "agent": { + "rulesCleanupFailed": "L'agent a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement." } }, "mdm": { @@ -180,10 +180,10 @@ } }, "prompts": { - "deleteMode": { - "title": "Supprimer le mode personnalisé", - "description": "Êtes-vous sûr de vouloir supprimer ce mode {{scope}} ? Cela supprimera également le dossier de règles associé à l'adresse : {{rulesFolderPath}}", - "descriptionNoRules": "Êtes-vous sûr de vouloir supprimer ce mode personnalisé ?", + "deleteAgent": { + "title": "Supprimer l'agent personnalisé", + "description": "Êtes-vous sûr de vouloir supprimer cet agent {{scope}} ? Cela supprimera également le dossier de règles associé à l'adresse : {{rulesFolderPath}}", + "descriptionNoRules": "Êtes-vous sûr de vouloir supprimer cet agent personnalisé ?", "confirm": "Supprimer" } }, diff --git a/src/i18n/locales/fr/marketplace.json b/src/i18n/locales/fr/marketplace.json index 7a42b0033e..b4a38d4039 100644 --- a/src/i18n/locales/fr/marketplace.json +++ b/src/i18n/locales/fr/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modes", + "agents": "Agents", "mcps": "Serveurs MCP", "match": "correspondance" }, "item-card": { - "type-mode": "Mode", + "type-agent": "Agent", "type-mcp": "Serveur MCP", "type-other": "Autre", "by-author": "par {{author}}", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index fae7c42be9..ee178da2bd 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।", "delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?", - "delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "अमान्य डेटा URI फॉर्मेट", @@ -69,7 +69,7 @@ "share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।", "share_not_enabled": "इस संगठन के लिए कार्य साझाकरण सक्षम नहीं है।", "share_task_not_found": "कार्य नहीं मिला या पहुंच अस्वीकृत।", - "mode_import_failed": "मोड आयात करने में विफल: {{error}}", + "agent_import_failed": "मोड आयात करने में विफल: {{error}}", "delete_rules_folder_failed": "नियम फ़ोल्डर हटाने में विफल: {{rulesFolderPath}}। त्रुटि: {{error}}", "command_not_found": "कमांड '{{name}}' नहीं मिला", "open_command_file": "कमांड फ़ाइल खोलने में विफल", @@ -109,8 +109,8 @@ "image_saved": "छवि {{path}} में सहेजी गई", "organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!", "public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!", - "mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया", - "mode_imported": "मोड सफलतापूर्वक आयात किया गया" + "agent_exported": "मोड '{{एजेंट}}' सफलतापूर्वक निर्यात किया गया", + "agent_imported": "मोड सफलतापूर्वक आयात किया गया" }, "answers": { "yes": "हां", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes फ़ाइल में लाइन {{line}} पर अमान्य YAML। कृपया जांचें:\n• सही इंडेंटेशन (टैब नहीं, स्पेस का उपयोग करें)\n• मैचिंग कोट्स और ब्रैकेट्स\n• वैध YAML सिंटैक्स", - "schemaValidationError": ".roomodes में अमान्य कस्टम मोड फॉर्मेट:\n{{issues}}", + "yamlParseError": ".rooagents फ़ाइल में लाइन {{line}} पर अमान्य YAML। कृपया जांचें:\n• सही इंडेंटेशन (टैब नहीं, स्पेस का उपयोग करें)\n• मैचिंग कोट्स और ब्रैकेट्स\n• वैध YAML सिंटैक्स", + "schemaValidationError": ".rooagents में अमान्य कस्टम मोड फॉर्मेट:\n{{issues}}", "invalidFormat": "अमान्य कस्टम मोड फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही YAML फॉर्मेट का पालन करती हैं।", "updateFailed": "कस्टम मोड अपडेट विफल: {{error}}", "deleteFailed": "कस्टम मोड डिलीट विफल: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "एजेंट": { "rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।" } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "कस्टम मोड हटाएं", "description": "क्या आप वाकई इस {{scope}} मोड को हटाना चाहते हैं? यह संबंधित नियम फ़ोल्डर को भी {{rulesFolderPath}} पर हटा देगा", "descriptionNoRules": "क्या आप वाकई इस कस्टम मोड को हटाना चाहते हैं?", diff --git a/src/i18n/locales/hi/marketplace.json b/src/i18n/locales/hi/marketplace.json index 94013c20e4..b03d8007c4 100644 --- a/src/i18n/locales/hi/marketplace.json +++ b/src/i18n/locales/hi/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "मोड्स", + "एजेंट": "मोड्स", "mcps": "MCP सर्वर", "match": "मैच" }, "item-card": { - "type-mode": "मोड", + "type-एजेंट": "मोड", "type-mcp": "MCP सर्वर", "type-other": "अन्य", "by-author": "{{author}} द्वारा", @@ -23,7 +23,7 @@ "type": { "label": "प्रकार", "all": "सभी प्रकार", - "mode": "मोड", + "एजेंट": "मोड", "mcpServer": "MCP सर्वर" }, "sort": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index eb2db5ac84..fe6edbdb13 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.", "delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?", - "delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Anda yakin ingin menghapus Agen {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Format data URI tidak valid", @@ -43,7 +43,7 @@ "list_api_config": "Gagal mendapatkan daftar konfigurasi api", "update_server_timeout": "Gagal memperbarui timeout server", "hmr_not_running": "Server pengembangan lokal tidak berjalan, HMR tidak akan bekerja. Silakan jalankan 'npm run dev' sebelum meluncurkan ekstensi untuk mengaktifkan HMR.", - "retrieve_current_mode": "Error: gagal mengambil mode saat ini dari state.", + "retrieve_current_mode": "Error: gagal mengambil Agen saat ini dari state.", "failed_delete_repo": "Gagal menghapus shadow repository atau branch yang terkait: {{error}}", "failed_remove_directory": "Gagal menghapus direktori tugas: {{error}}", "custom_storage_path_unusable": "Path penyimpanan kustom \"{{path}}\" tidak dapat digunakan, akan menggunakan path default", @@ -69,7 +69,7 @@ "share_auth_required": "Autentikasi diperlukan. Silakan masuk untuk berbagi tugas.", "share_not_enabled": "Berbagi tugas tidak diaktifkan untuk organisasi ini.", "share_task_not_found": "Tugas tidak ditemukan atau akses ditolak.", - "mode_import_failed": "Gagal mengimpor mode: {{error}}", + "agent_import_failed": "Gagal mengimpor Agen: {{error}}", "delete_rules_folder_failed": "Gagal menghapus folder aturan: {{rulesFolderPath}}. Error: {{error}}", "command_not_found": "Perintah '{{name}}' tidak ditemukan", "open_command_file": "Gagal membuka file perintah", @@ -109,8 +109,8 @@ "image_saved": "Gambar disimpan ke {{path}}", "organization_share_link_copied": "Tautan berbagi organisasi disalin ke clipboard!", "public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!", - "mode_exported": "Mode '{{mode}}' berhasil diekspor", - "mode_imported": "Mode berhasil diimpor" + "agent_exported": "Agen '{{Agen}}' berhasil diekspor", + "agent_imported": "Agen berhasil diimpor" }, "answers": { "yes": "Ya", @@ -150,17 +150,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML tidak valid dalam file .roomodes pada baris {{line}}. Silakan periksa:\n• Indentasi yang benar (gunakan spasi, bukan tab)\n• Tanda kutip dan kurung yang cocok\n• Sintaks YAML yang valid", - "schemaValidationError": "Format mode kustom tidak valid dalam .roomodes:\n{{issues}}", - "invalidFormat": "Format mode kustom tidak valid. Pastikan pengaturan kamu mengikuti format YAML yang benar.", - "updateFailed": "Gagal memperbarui mode kustom: {{error}}", - "deleteFailed": "Gagal menghapus mode kustom: {{error}}", - "resetFailed": "Gagal mereset mode kustom: {{error}}", - "modeNotFound": "Kesalahan tulis: Mode tidak ditemukan", - "noWorkspaceForProject": "Tidak ditemukan folder workspace untuk mode khusus proyek", - "rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." + "yamlParseError": "YAML tidak valid dalam file .rooagents pada baris {{line}}. Silakan periksa:\n• Indentasi yang benar (gunakan spasi, bukan tab)\n• Tanda kutip dan kurung yang cocok\n• Sintaks YAML yang valid", + "schemaValidationError": "Format Agen kustom tidak valid dalam .rooagents:\n{{issues}}", + "invalidFormat": "Format Agen kustom tidak valid. Pastikan pengaturan kamu mengikuti format YAML yang benar.", + "updateFailed": "Gagal memperbarui Agen kustom: {{error}}", + "deleteFailed": "Gagal menghapus Agen kustom: {{error}}", + "resetFailed": "Gagal mereset Agen kustom: {{error}}", + "modeNotFound": "Kesalahan tulis: Agen tidak ditemukan", + "noWorkspaceForProject": "Tidak ditemukan folder workspace untuk Agen khusus proyek", + "rulesCleanupFailed": "Agen berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." }, "scope": { "project": "proyek", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." + "Agen": { + "rulesCleanupFailed": "Agen berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual." } }, "mdm": { @@ -180,10 +180,10 @@ } }, "prompts": { - "deleteMode": { - "title": "Hapus Mode Kustom", - "description": "Anda yakin ingin menghapus mode {{scope}} ini? Ini juga akan menghapus folder aturan terkait di: {{rulesFolderPath}}", - "descriptionNoRules": "Anda yakin ingin menghapus mode kustom ini?", + "deleteAgent": { + "title": "Hapus Agen Kustom", + "description": "Anda yakin ingin menghapus Agen {{scope}} ini? Ini juga akan menghapus folder aturan terkait di: {{rulesFolderPath}}", + "descriptionNoRules": "Anda yakin ingin menghapus Agen kustom ini?", "confirm": "Hapus" } }, diff --git a/src/i18n/locales/id/marketplace.json b/src/i18n/locales/id/marketplace.json index 77d93973a9..5e154aa4c9 100644 --- a/src/i18n/locales/id/marketplace.json +++ b/src/i18n/locales/id/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Mode", + "Agen": "Agen", "mcps": "Server MCP", "match": "cocok" }, "item-card": { - "type-mode": "Mode", + "type-Agen": "Agen", "type-mcp": "Server MCP", "type-other": "Lainnya", "by-author": "oleh {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Tipe", "all": "Semua Tipe", - "mode": "Mode", + "Agen": "Agen", "mcpServer": "Server MCP" }, "sort": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a7ef4b075a..5a892610b0 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Sei sicuro di voler reimpostare tutti gli stati e l'archiviazione segreta nell'estensione? Questa azione non può essere annullata.", "delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?", - "delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Formato URI dati non valido", @@ -69,7 +69,7 @@ "share_auth_required": "Autenticazione richiesta. Accedi per condividere le attività.", "share_not_enabled": "La condivisione delle attività non è abilitata per questa organizzazione.", "share_task_not_found": "Attività non trovata o accesso negato.", - "mode_import_failed": "Importazione della modalità non riuscita: {{error}}", + "agent_import_failed": "Importazione della modalità non riuscita: {{error}}", "delete_rules_folder_failed": "Impossibile eliminare la cartella delle regole: {{rulesFolderPath}}. Errore: {{error}}", "command_not_found": "Comando '{{name}}' non trovato", "open_command_file": "Impossibile aprire il file di comando", @@ -109,8 +109,8 @@ "image_saved": "Immagine salvata in {{path}}", "organization_share_link_copied": "Link di condivisione organizzazione copiato negli appunti!", "public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!", - "mode_exported": "Modalità '{{mode}}' esportata con successo", - "mode_imported": "Modalità importata con successo" + "agent_exported": "Modalità '{{Agente}}' esportata con successo", + "agent_imported": "Modalità importata con successo" }, "answers": { "yes": "Sì", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML non valido nel file .roomodes alla riga {{line}}. Controlla:\n• Indentazione corretta (usa spazi, non tab)\n• Virgolette e parentesi corrispondenti\n• Sintassi YAML valida", - "schemaValidationError": "Formato modalità personalizzate non valido in .roomodes:\n{{issues}}", + "yamlParseError": "YAML non valido nel file .rooagents alla riga {{line}}. Controlla:\n• Indentazione corretta (usa spazi, non tab)\n• Virgolette e parentesi corrispondenti\n• Sintassi YAML valida", + "schemaValidationError": "Formato modalità personalizzate non valido in .rooagents:\n{{issues}}", "invalidFormat": "Formato modalità personalizzate non valido. Assicurati che le tue impostazioni seguano il formato YAML corretto.", "updateFailed": "Aggiornamento modalità personalizzata fallito: {{error}}", "deleteFailed": "Eliminazione modalità personalizzata fallita: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Agente": { "rulesCleanupFailed": "La modalità è stata rimossa con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Elimina Modalità Personalizzata", "description": "Sei sicuro di voler eliminare questa modalità {{scope}}? Questo eliminerà anche la cartella delle regole associata a: {{rulesFolderPath}}", "descriptionNoRules": "Sei sicuro di voler eliminare questa modalità personalizzata?", diff --git a/src/i18n/locales/it/marketplace.json b/src/i18n/locales/it/marketplace.json index 3cdcd2b76c..c3a788353c 100644 --- a/src/i18n/locales/it/marketplace.json +++ b/src/i18n/locales/it/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modalità", + "Agenti": "Modalità", "mcps": "Server MCP", "match": "corrispondenza" }, "item-card": { - "type-mode": "Modalità", + "type-Agente": "Modalità", "type-mcp": "Server MCP", "type-other": "Altro", "by-author": "di {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Tipo", "all": "Tutti i tipi", - "mode": "Modalità", + "Agente": "Modalità", "mcpServer": "Server MCP" }, "sort": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 6e7e0b8a3e..4210958e40 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "拡張機能のすべての状態とシークレットストレージをリセットしてもよろしいですか?この操作は元に戻せません。", "delete_config_profile": "この設定プロファイルを削除してもよろしいですか?", - "delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "この{scope}エージェントを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "データURIフォーマットが無効です", @@ -43,7 +43,7 @@ "list_api_config": "API設定リストの取得に失敗しました", "update_server_timeout": "サーバータイムアウトの更新に失敗しました", "hmr_not_running": "ローカル開発サーバーが実行されていないため、HMRは機能しません。HMRを有効にするには、拡張機能を起動する前に'npm run dev'を実行してください。", - "retrieve_current_mode": "現在のモードを状態から取得する際にエラーが発生しました。", + "retrieve_current_agent": "現在のエージェントを状態から取得する際にエラーが発生しました。", "failed_delete_repo": "関連するシャドウリポジトリまたはブランチの削除に失敗しました:{{error}}", "failed_remove_directory": "タスクディレクトリの削除に失敗しました:{{error}}", "custom_storage_path_unusable": "カスタムストレージパス \"{{path}}\" が使用できないため、デフォルトパスを使用します", @@ -69,7 +69,7 @@ "share_auth_required": "認証が必要です。タスクを共有するにはサインインしてください。", "share_not_enabled": "この組織ではタスク共有が有効になっていません。", "share_task_not_found": "タスクが見つからないか、アクセスが拒否されました。", - "mode_import_failed": "モードのインポートに失敗しました:{{error}}", + "agent_import_failed": "エージェントのインポートに失敗しました:{{error}}", "delete_rules_folder_failed": "ルールフォルダの削除に失敗しました:{{rulesFolderPath}}。エラー:{{error}}", "command_not_found": "コマンド '{{name}}' が見つかりません", "open_command_file": "コマンドファイルを開けませんでした", @@ -109,8 +109,8 @@ "image_saved": "画像を{{path}}に保存しました", "organization_share_link_copied": "組織共有リンクがクリップボードにコピーされました!", "public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!", - "mode_exported": "モード「{{mode}}」が正常にエクスポートされました", - "mode_imported": "モードが正常にインポートされました" + "agent_exported": "エージェント「{{agent}}」が正常にエクスポートされました", + "agent_imported": "エージェントが正常にインポートされました" }, "answers": { "yes": "はい", @@ -150,17 +150,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文", - "schemaValidationError": ".roomodes のカスタムモード形式が無効です:\n{{issues}}", - "invalidFormat": "カスタムモード形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。", - "updateFailed": "カスタムモードの更新に失敗しました:{{error}}", - "deleteFailed": "カスタムモードの削除に失敗しました:{{error}}", - "resetFailed": "カスタムモードのリセットに失敗しました:{{error}}", - "modeNotFound": "書き込みエラー:モードが見つかりません", - "noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません", - "rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" + "yamlParseError": ".rooagents ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文", + "schemaValidationError": ".rooagents のカスタムエージェント形式が無効です:\n{{issues}}", + "invalidFormat": "カスタムエージェント形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。", + "updateFailed": "カスタムエージェントの更新に失敗しました:{{error}}", + "deleteFailed": "カスタムエージェントの削除に失敗しました:{{error}}", + "resetFailed": "カスタムエージェントのリセットに失敗しました:{{error}}", + "agentNotFound": "書き込みエラー:エージェントが見つかりません", + "noWorkspaceForProject": "プロジェクト固有エージェント用のワークスペースフォルダーが見つかりません", + "rulesCleanupFailed": "エージェントは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" }, "scope": { "project": "プロジェクト", @@ -168,8 +168,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" + "agent": { + "rulesCleanupFailed": "エージェントは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。" } }, "mdm": { diff --git a/src/i18n/locales/ja/marketplace.json b/src/i18n/locales/ja/marketplace.json index 26cff2ade9..d782e6042b 100644 --- a/src/i18n/locales/ja/marketplace.json +++ b/src/i18n/locales/ja/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "モード", + "agents": "エージェント", "mcps": "MCPサーバー", "match": "マッチ" }, "item-card": { - "type-mode": "モード", + "type-agent": "エージェント", "type-mcp": "MCPサーバー", "type-other": "その他", "by-author": "{{author}}による", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 1d0a5f3c4a..311c9ee6ae 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "확장 프로그램의 모든 상태와 보안 저장소를 재설정하시겠습니까? 이 작업은 취소할 수 없습니다.", "delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?", - "delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "잘못된 데이터 URI 형식", @@ -69,7 +69,7 @@ "share_auth_required": "인증이 필요합니다. 작업을 공유하려면 로그인하세요.", "share_not_enabled": "이 조직에서는 작업 공유가 활성화되지 않았습니다.", "share_task_not_found": "작업을 찾을 수 없거나 액세스가 거부되었습니다.", - "mode_import_failed": "모드 가져오기 실패: {{error}}", + "agent_import_failed": "모드 가져오기 실패: {{error}}", "delete_rules_folder_failed": "규칙 폴더 삭제 실패: {{rulesFolderPath}}. 오류: {{error}}", "command_not_found": "'{{name}}' 명령을 찾을 수 없습니다", "open_command_file": "명령 파일을 열 수 없습니다", @@ -109,8 +109,8 @@ "image_saved": "이미지가 {{path}}에 저장되었습니다", "organization_share_link_copied": "조직 공유 링크가 클립보드에 복사되었습니다!", "public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!", - "mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다", - "mode_imported": "모드를 성공적으로 가져왔습니다" + "agent_exported": "'{{에이전트}}' 모드가 성공적으로 내보내졌습니다", + "agent_imported": "모드를 성공적으로 가져왔습니다" }, "answers": { "yes": "예", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문", - "schemaValidationError": ".roomodes의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}", + "yamlParseError": ".rooagents 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문", + "schemaValidationError": ".rooagents의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}", "invalidFormat": "사용자 정의 모드 형식이 유효하지 않습니다. 설정이 올바른 YAML 형식을 따르는지 확인하세요.", "updateFailed": "사용자 정의 모드 업데이트 실패: {{error}}", "deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "에이전트": { "rulesCleanupFailed": "모드가 성공적으로 제거되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "사용자 정의 모드 삭제", "description": "이 {{scope}} 모드를 삭제하시겠습니까? 이렇게 하면 {{rulesFolderPath}}의 관련 규칙 폴더도 삭제됩니다.", "descriptionNoRules": "이 사용자 정의 모드를 삭제하시겠습니까?", diff --git a/src/i18n/locales/ko/marketplace.json b/src/i18n/locales/ko/marketplace.json index 52bd03edf7..f7c9e45ca0 100644 --- a/src/i18n/locales/ko/marketplace.json +++ b/src/i18n/locales/ko/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "모드", + "에이전트": "모드", "mcps": "MCP 서버", "match": "일치" }, "item-card": { - "type-mode": "모드", + "type-에이전트": "모드", "type-mcp": "MCP 서버", "type-other": "기타", "by-author": "{{author}} 작성", @@ -23,7 +23,7 @@ "type": { "label": "유형", "all": "모든 유형", - "mode": "모드", + "에이전트": "모드", "mcpServer": "MCP 서버" }, "sort": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index bb7d3c0f23..1d2b0d6ffe 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Weet je zeker dat je alle status en geheime opslag in de extensie wilt resetten? Dit kan niet ongedaan worden gemaakt.", "delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?", - "delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Ongeldig data-URI-formaat", @@ -69,7 +69,7 @@ "share_auth_required": "Authenticatie vereist. Log in om taken te delen.", "share_not_enabled": "Taken delen is niet ingeschakeld voor deze organisatie.", "share_task_not_found": "Taak niet gevonden of toegang geweigerd.", - "mode_import_failed": "Importeren van modus mislukt: {{error}}", + "agent_import_failed": "Importeren van modus mislukt: {{error}}", "delete_rules_folder_failed": "Kan regelmap niet verwijderen: {{rulesFolderPath}}. Fout: {{error}}", "command_not_found": "Opdracht '{{name}}' niet gevonden", "open_command_file": "Kan opdrachtbestand niet openen", @@ -109,8 +109,8 @@ "image_saved": "Afbeelding opgeslagen naar {{path}}", "organization_share_link_copied": "Organisatie deel-link gekopieerd naar klembord!", "public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!", - "mode_exported": "Modus '{{mode}}' succesvol geëxporteerd", - "mode_imported": "Modus succesvol geïmporteerd" + "agent_exported": "Modus '{{Agent}}' succesvol geëxporteerd", + "agent_imported": "Modus succesvol geïmporteerd" }, "answers": { "yes": "Ja", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "Ongeldige YAML in .roomodes bestand op regel {{line}}. Controleer:\n• Juiste inspringing (gebruik spaties, geen tabs)\n• Overeenkomende aanhalingstekens en haakjes\n• Geldige YAML syntaxis", - "schemaValidationError": "Ongeldig aangepaste modi formaat in .roomodes:\n{{issues}}", + "yamlParseError": "Ongeldige YAML in .rooagents bestand op regel {{line}}. Controleer:\n• Juiste inspringing (gebruik spaties, geen tabs)\n• Overeenkomende aanhalingstekens en haakjes\n• Geldige YAML syntaxis", + "schemaValidationError": "Ongeldig aangepaste modi formaat in .rooagents:\n{{issues}}", "invalidFormat": "Ongeldig aangepaste modi formaat. Zorg ervoor dat je instellingen het juiste YAML formaat volgen.", "updateFailed": "Aangepaste modus bijwerken mislukt: {{error}}", "deleteFailed": "Aangepaste modus verwijderen mislukt: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Agent": { "rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Aangepaste modus verwijderen", "description": "Weet je zeker dat je deze {{scope}}-modus wilt verwijderen? Dit zal ook de bijbehorende regelsmap op {{rulesFolderPath}} verwijderen", "descriptionNoRules": "Weet je zeker dat je deze aangepaste modus wilt verwijderen?", diff --git a/src/i18n/locales/nl/marketplace.json b/src/i18n/locales/nl/marketplace.json index 5628b8f628..1e3cc23235 100644 --- a/src/i18n/locales/nl/marketplace.json +++ b/src/i18n/locales/nl/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modi", + "Agenten": "Modi", "mcps": "MCP Servers", "match": "overeenkomst" }, "item-card": { - "type-mode": "Modus", + "type-Agent": "Modus", "type-mcp": "MCP Server", "type-other": "Andere", "by-author": "door {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Type", "all": "Alle types", - "mode": "Modus", + "Agent": "Modus", "mcpServer": "MCP Server" }, "sort": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 953f52ea79..82a0c71c9a 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Czy na pewno chcesz zresetować wszystkie stany i tajne magazyny w rozszerzeniu? Tej operacji nie można cofnąć.", "delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?", - "delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Nieprawidłowy format URI danych", @@ -69,7 +69,7 @@ "share_auth_required": "Wymagana autoryzacja. Zaloguj się, aby udostępniać zadania.", "share_not_enabled": "Udostępnianie zadań nie jest włączone dla tej organizacji.", "share_task_not_found": "Zadanie nie znalezione lub dostęp odmówiony.", - "mode_import_failed": "Import trybu nie powiódł się: {{error}}", + "agent_import_failed": "Import trybu nie powiódł się: {{error}}", "delete_rules_folder_failed": "Nie udało się usunąć folderu reguł: {{rulesFolderPath}}. Błąd: {{error}}", "command_not_found": "Polecenie '{{name}}' nie zostało znalezione", "open_command_file": "Nie udało się otworzyć pliku polecenia", @@ -109,8 +109,8 @@ "image_saved": "Obraz zapisany w {{path}}", "organization_share_link_copied": "Link udostępniania organizacji skopiowany do schowka!", "public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!", - "mode_exported": "Tryb '{{mode}}' pomyślnie wyeksportowany", - "mode_imported": "Tryb pomyślnie zaimportowany" + "agent_exported": "Tryb '{{Agent}}' pomyślnie wyeksportowany", + "agent_imported": "Tryb pomyślnie zaimportowany" }, "answers": { "yes": "Tak", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "Nieprawidłowy YAML w pliku .roomodes w linii {{line}}. Sprawdź:\n• Prawidłowe wcięcia (używaj spacji, nie tabulatorów)\n• Pasujące cudzysłowy i nawiasy\n• Prawidłową składnię YAML", - "schemaValidationError": "Nieprawidłowy format trybów niestandardowych w .roomodes:\n{{issues}}", + "yamlParseError": "Nieprawidłowy YAML w pliku .rooagents w linii {{line}}. Sprawdź:\n• Prawidłowe wcięcia (używaj spacji, nie tabulatorów)\n• Pasujące cudzysłowy i nawiasy\n• Prawidłową składnię YAML", + "schemaValidationError": "Nieprawidłowy format trybów niestandardowych w .rooagents:\n{{issues}}", "invalidFormat": "Nieprawidłowy format trybów niestandardowych. Upewnij się, że twoje ustawienia są zgodne z prawidłowym formatem YAML.", "updateFailed": "Aktualizacja trybu niestandardowego nie powiodła się: {{error}}", "deleteFailed": "Usunięcie trybu niestandardowego nie powiodło się: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Agent": { "rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Usuń tryb niestandardowy", "description": "Czy na pewno chcesz usunąć ten tryb {{scope}}? Spowoduje to również usunięcie powiązanego folderu z regułami w {{rulesFolderPath}}", "descriptionNoRules": "Czy na pewno chcesz usunąć ten tryb niestandardowy?", diff --git a/src/i18n/locales/pl/marketplace.json b/src/i18n/locales/pl/marketplace.json index 029dbd95a3..ac9a1d8ae7 100644 --- a/src/i18n/locales/pl/marketplace.json +++ b/src/i18n/locales/pl/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Tryby", + "Agenci": "Tryby", "mcps": "Serwery MCP", "match": "dopasowanie" }, "item-card": { - "type-mode": "Tryb", + "type-Agent": "Tryb", "type-mcp": "Serwer MCP", "type-other": "Inne", "by-author": "przez {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Typ", "all": "Wszystkie typy", - "mode": "Tryb", + "Agent": "Tryb", "mcpServer": "Serwer MCP" }, "sort": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 21aca727a1..2938a3937b 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -21,7 +21,7 @@ "confirmation": { "reset_state": "Tem certeza de que deseja redefinir todo o estado e armazenamento secreto na extensão? Isso não pode ser desfeito.", "delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?", - "delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Formato de URI de dados inválido", @@ -73,7 +73,7 @@ "share_auth_required": "Autenticação necessária. Faça login para compartilhar tarefas.", "share_not_enabled": "O compartilhamento de tarefas não está habilitado para esta organização.", "share_task_not_found": "Tarefa não encontrada ou acesso negado.", - "mode_import_failed": "Falha ao importar o modo: {{error}}", + "agent_import_failed": "Falha ao importar o modo: {{error}}", "delete_rules_folder_failed": "Falha ao excluir pasta de regras: {{rulesFolderPath}}. Erro: {{error}}", "command_not_found": "Comando '{{name}}' não encontrado", "open_command_file": "Falha ao abrir arquivo de comando", @@ -113,8 +113,8 @@ "image_saved": "Imagem salva em {{path}}", "organization_share_link_copied": "Link de compartilhamento da organização copiado para a área de transferência!", "public_share_link_copied": "Link de compartilhamento público copiado para a área de transferência!", - "mode_exported": "Modo '{{mode}}' exportado com sucesso", - "mode_imported": "Modo importado com sucesso" + "agent_exported": "Modo '{{Agente}}' exportado com sucesso", + "agent_imported": "Modo importado com sucesso" }, "answers": { "yes": "Sim", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML inválido no arquivo .roomodes na linha {{line}}. Verifique:\n• Indentação correta (use espaços, não tabs)\n• Aspas e colchetes correspondentes\n• Sintaxe YAML válida", - "schemaValidationError": "Formato de modos personalizados inválido em .roomodes:\n{{issues}}", + "yamlParseError": "YAML inválido no arquivo .rooagents na linha {{line}}. Verifique:\n• Indentação correta (use espaços, não tabs)\n• Aspas e colchetes correspondentes\n• Sintaxe YAML válida", + "schemaValidationError": "Formato de modos personalizados inválido em .rooagents:\n{{issues}}", "invalidFormat": "Formato de modos personalizados inválido. Certifique-se de que suas configurações seguem o formato YAML correto.", "updateFailed": "Falha ao atualizar modo personalizado: {{error}}", "deleteFailed": "Falha ao excluir modo personalizado: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Agente": { "rulesCleanupFailed": "O modo foi removido com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Excluir Modo Personalizado", "description": "Tem certeza de que deseja excluir este modo {{scope}}? Isso também excluirá a pasta de regras associada em: {{rulesFolderPath}}", "descriptionNoRules": "Tem certeza de que deseja excluir este modo personalizado?", diff --git a/src/i18n/locales/pt-BR/marketplace.json b/src/i18n/locales/pt-BR/marketplace.json index b0af013888..db61fe9c61 100644 --- a/src/i18n/locales/pt-BR/marketplace.json +++ b/src/i18n/locales/pt-BR/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modos", + "Agentes": "Modos", "mcps": "Servidores MCP", "match": "correspondência" }, "item-card": { - "type-mode": "Modo", + "type-Agente": "Modo", "type-mcp": "Servidor MCP", "type-other": "Outro", "by-author": "por {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Tipo", "all": "Todos os tipos", - "mode": "Modo", + "Agente": "Modo", "mcpServer": "Servidor MCP" }, "sort": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 30913e16e9..b6f3aea566 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Вы уверены, что хотите сбросить все состояние и секретное хранилище в расширении? Это действие нельзя отменить.", "delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?", - "delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Неверный формат URI данных", @@ -69,7 +69,7 @@ "share_auth_required": "Требуется аутентификация. Войдите в систему для совместного доступа к задачам.", "share_not_enabled": "Совместный доступ к задачам не включен для этой организации.", "share_task_not_found": "Задача не найдена или доступ запрещен.", - "mode_import_failed": "Не удалось импортировать режим: {{error}}", + "agent_import_failed": "Не удалось импортировать режим: {{error}}", "delete_rules_folder_failed": "Не удалось удалить папку правил: {{rulesFolderPath}}. Ошибка: {{error}}", "command_not_found": "Команда '{{name}}' не найдена", "open_command_file": "Не удалось открыть файл команды", @@ -109,8 +109,8 @@ "image_saved": "Изображение сохранено в {{path}}", "organization_share_link_copied": "Ссылка для совместного доступа организации скопирована в буфер обмена!", "public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!", - "mode_exported": "Режим '{{mode}}' успешно экспортирован", - "mode_imported": "Режим успешно импортирован" + "agent_exported": "Режим '{{Агент}}' успешно экспортирован", + "agent_imported": "Режим успешно импортирован" }, "answers": { "yes": "Да", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "Недопустимый YAML в файле .roomodes на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML", - "schemaValidationError": "Недопустимый формат пользовательских режимов в .roomodes:\n{{issues}}", + "yamlParseError": "Недопустимый YAML в файле .rooagents на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML", + "schemaValidationError": "Недопустимый формат пользовательских режимов в .rooagents:\n{{issues}}", "invalidFormat": "Недопустимый формат пользовательских режимов. Убедись, что твои настройки соответствуют правильному формату YAML.", "updateFailed": "Не удалось обновить пользовательский режим: {{error}}", "deleteFailed": "Не удалось удалить пользовательский режим: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Агент": { "rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Удалить пользовательский режим", "description": "Вы уверены, что хотите удалить этот режим {{scope}}? Это также удалит связанную папку правил по адресу: {{rulesFolderPath}}", "descriptionNoRules": "Вы уверены, что хотите удалить этот пользовательский режим?", diff --git a/src/i18n/locales/ru/marketplace.json b/src/i18n/locales/ru/marketplace.json index a84b1ce3e9..dc70b29b15 100644 --- a/src/i18n/locales/ru/marketplace.json +++ b/src/i18n/locales/ru/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Режимы", + "Агенты": "Режимы", "mcps": "MCP серверы", "match": "совпадение" }, "item-card": { - "type-mode": "Режим", + "type-Агент": "Режим", "type-mcp": "MCP сервер", "type-other": "Другое", "by-author": "от {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Тип", "all": "Все типы", - "mode": "Режим", + "Агент": "Режим", "mcpServer": "MCP сервер" }, "sort": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 6892c7c8f1..2f77151ab9 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Uzantıdaki tüm durumları ve gizli depolamayı sıfırlamak istediğinizden emin misiniz? Bu işlem geri alınamaz.", "delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?", - "delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Geçersiz veri URI formatı", @@ -69,7 +69,7 @@ "share_auth_required": "Kimlik doğrulama gerekli. Görevleri paylaşmak için lütfen giriş yapın.", "share_not_enabled": "Bu kuruluş için görev paylaşımı etkinleştirilmemiş.", "share_task_not_found": "Görev bulunamadı veya erişim reddedildi.", - "mode_import_failed": "Mod içe aktarılamadı: {{error}}", + "agent_import_failed": "Mod içe aktarılamadı: {{error}}", "delete_rules_folder_failed": "Kurallar klasörü silinemedi: {{rulesFolderPath}}. Hata: {{error}}", "command_not_found": "'{{name}}' komutu bulunamadı", "open_command_file": "Komut dosyası açılamadı", @@ -109,8 +109,8 @@ "image_saved": "Resim {{path}} konumuna kaydedildi", "organization_share_link_copied": "Kuruluş paylaşım bağlantısı panoya kopyalandı!", "public_share_link_copied": "Herkese açık paylaşım bağlantısı panoya kopyalandı!", - "mode_exported": "'{{mode}}' modu başarıyla dışa aktarıldı", - "mode_imported": "Mod başarıyla içe aktarıldı" + "agent_exported": "'{{Ajan}}' modu başarıyla dışa aktarıldı", + "agent_imported": "Mod başarıyla içe aktarıldı" }, "answers": { "yes": "Evet", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes dosyasının {{line}}. satırında geçersiz YAML. Kontrol et:\n• Doğru girinti (tab değil boşluk kullan)\n• Eşleşen tırnak işaretleri ve parantezler\n• Geçerli YAML sözdizimi", - "schemaValidationError": ".roomodes'ta geçersiz özel mod formatı:\n{{issues}}", + "yamlParseError": ".rooagents dosyasının {{line}}. satırında geçersiz YAML. Kontrol et:\n• Doğru girinti (tab değil boşluk kullan)\n• Eşleşen tırnak işaretleri ve parantezler\n• Geçerli YAML sözdizimi", + "schemaValidationError": ".rooagents'ta geçersiz özel mod formatı:\n{{issues}}", "invalidFormat": "Geçersiz özel mod formatı. Ayarlarının doğru YAML formatını takip ettiğinden emin ol.", "updateFailed": "Özel mod güncellemesi başarısız: {{error}}", "deleteFailed": "Özel mod silme başarısız: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Ajan": { "rulesCleanupFailed": "Mod başarıyla kaldırıldı, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Özel Modu Sil", "description": "Bu {{scope}} modunu silmek istediğinizden emin misiniz? Bu, {{rulesFolderPath}} adresindeki ilişkili kurallar klasörünü de silecektir", "descriptionNoRules": "Bu özel modu silmek istediğinizden emin misiniz?", diff --git a/src/i18n/locales/tr/marketplace.json b/src/i18n/locales/tr/marketplace.json index b08381d71c..f7c0d897c2 100644 --- a/src/i18n/locales/tr/marketplace.json +++ b/src/i18n/locales/tr/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Modlar", + "Ajanlar": "Modlar", "mcps": "MCP Sunucuları", "match": "eşleşme" }, "item-card": { - "type-mode": "Mod", + "type-Ajan": "Mod", "type-mcp": "MCP Sunucusu", "type-other": "Diğer", "by-author": "{{author}} tarafından", @@ -23,7 +23,7 @@ "type": { "label": "Tür", "all": "Tüm Türler", - "mode": "Mod", + "Ajan": "Mod", "mcpServer": "MCP Sunucusu" }, "sort": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index f88120098d..5e32663fd3 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "Bạn có chắc chắn muốn đặt lại tất cả trạng thái và lưu trữ bí mật trong tiện ích mở rộng không? Hành động này không thể hoàn tác.", "delete_config_profile": "Bạn có chắc chắn muốn xóa hồ sơ cấu hình này không?", - "delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ", @@ -69,7 +69,7 @@ "share_auth_required": "Cần xác thực. Vui lòng đăng nhập để chia sẻ nhiệm vụ.", "share_not_enabled": "Chia sẻ nhiệm vụ không được bật cho tổ chức này.", "share_task_not_found": "Không tìm thấy nhiệm vụ hoặc truy cập bị từ chối.", - "mode_import_failed": "Nhập chế độ thất bại: {{error}}", + "agent_import_failed": "Nhập chế độ thất bại: {{error}}", "delete_rules_folder_failed": "Không thể xóa thư mục quy tắc: {{rulesFolderPath}}. Lỗi: {{error}}", "command_not_found": "Không tìm thấy lệnh '{{name}}'", "open_command_file": "Không thể mở tệp lệnh", @@ -109,8 +109,8 @@ "image_saved": "Hình ảnh đã được lưu vào {{path}}", "organization_share_link_copied": "Liên kết chia sẻ tổ chức đã được sao chép vào clipboard!", "public_share_link_copied": "Liên kết chia sẻ công khai đã được sao chép vào clipboard!", - "mode_exported": "Chế độ '{{mode}}' đã được xuất thành công", - "mode_imported": "Chế độ đã được nhập thành công" + "agent_exported": "Chế độ '{{Đại lý}}' đã được xuất thành công", + "agent_imported": "Chế độ đã được nhập thành công" }, "answers": { "yes": "Có", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": "YAML không hợp lệ trong tệp .roomodes tại dòng {{line}}. Vui lòng kiểm tra:\n• Thụt lề đúng (dùng dấu cách, không dùng tab)\n• Dấu ngoặc kép và ngoặc đơn khớp nhau\n• Cú pháp YAML hợp lệ", - "schemaValidationError": "Định dạng chế độ tùy chỉnh không hợp lệ trong .roomodes:\n{{issues}}", + "yamlParseError": "YAML không hợp lệ trong tệp .rooagents tại dòng {{line}}. Vui lòng kiểm tra:\n• Thụt lề đúng (dùng dấu cách, không dùng tab)\n• Dấu ngoặc kép và ngoặc đơn khớp nhau\n• Cú pháp YAML hợp lệ", + "schemaValidationError": "Định dạng chế độ tùy chỉnh không hợp lệ trong .rooagents:\n{{issues}}", "invalidFormat": "Định dạng chế độ tùy chỉnh không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng YAML đúng.", "updateFailed": "Cập nhật chế độ tùy chỉnh thất bại: {{error}}", "deleteFailed": "Xóa chế độ tùy chỉnh thất bại: {{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "Đại lý": { "rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công." } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "Xóa chế độ tùy chỉnh", "description": "Bạn có chắc chắn muốn xóa chế độ {{scope}} này không? Thao tác này cũng θα xóa thư mục quy tắc liên quan tại {{rulesFolderPath}}", "translations": { diff --git a/src/i18n/locales/vi/marketplace.json b/src/i18n/locales/vi/marketplace.json index be39054309..feaa1f4d65 100644 --- a/src/i18n/locales/vi/marketplace.json +++ b/src/i18n/locales/vi/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "Chế độ", + "Đại lý": "Chế độ", "mcps": "Máy chủ MCP", "match": "khớp" }, "item-card": { - "type-mode": "Chế độ", + "type-Đại lý": "Chế độ", "type-mcp": "Máy chủ MCP", "type-other": "Khác", "by-author": "bởi {{author}}", @@ -23,7 +23,7 @@ "type": { "label": "Loại", "all": "Tất cả loại", - "mode": "Chế độ", + "Đại lý": "Chế độ", "mcpServer": "Máy chủ MCP" }, "sort": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index e81b7d589a..a49afa651a 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "您确定要重置扩展中的所有状态和密钥存储吗?此操作无法撤消。", "delete_config_profile": "您确定要删除此配置文件吗?", - "delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "您确定要删除此 {scope} 代理吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}" }, "errors": { "invalid_mcp_config": "项目MCP配置格式无效", @@ -48,7 +48,7 @@ "list_api_config": "获取API配置列表失败", "update_server_timeout": "更新服务器超时设置失败", "hmr_not_running": "本地开发服务器未运行,HMR将不起作用。请在启动扩展前运行'npm run dev'以启用HMR。", - "retrieve_current_mode": "从状态中检索当前模式失败。", + "retrieve_current_agent": "从状态中检索当前代理失败。", "failed_delete_repo": "删除关联的影子仓库或分支失败:{{error}}", "failed_remove_directory": "删除任务目录失败:{{error}}", "custom_storage_path_unusable": "自定义存储路径 \"{{path}}\" 不可用,将使用默认路径", @@ -74,7 +74,7 @@ "share_auth_required": "需要身份验证。请登录以分享任务。", "share_not_enabled": "此组织未启用任务分享功能。", "share_task_not_found": "未找到任务或访问被拒绝。", - "mode_import_failed": "导入模式失败:{{error}}", + "agent_import_failed": "导入代理失败:{{error}}", "delete_rules_folder_failed": "删除规则文件夹失败:{{rulesFolderPath}}。错误:{{error}}", "command_not_found": "未找到命令 '{{name}}'", "open_command_file": "打开命令文件失败", @@ -114,8 +114,8 @@ "image_saved": "图片已保存到 {{path}}", "organization_share_link_copied": "组织分享链接已复制到剪贴板!", "public_share_link_copied": "公开分享链接已复制到剪贴板!", - "mode_exported": "模式 '{{mode}}' 已成功导出", - "mode_imported": "模式已成功导入" + "agent_exported": "代理 '{{agent}}' 已成功导出", + "agent_imported": "代理已成功导入" }, "answers": { "yes": "是", @@ -155,17 +155,17 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法", - "schemaValidationError": ".roomodes 中自定义模式格式无效:\n{{issues}}", - "invalidFormat": "自定义模式格式无效。请确保你的设置遵循正确的 YAML 格式。", - "updateFailed": "更新自定义模式失败:{{error}}", - "deleteFailed": "删除自定义模式失败:{{error}}", - "resetFailed": "重置自定义模式失败:{{error}}", - "modeNotFound": "写入错误:未找到模式", - "noWorkspaceForProject": "未找到项目特定模式的工作区文件夹", - "rulesCleanupFailed": "模式删除成功,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" + "yamlParseError": ".rooagents 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法", + "schemaValidationError": ".rooagents 中自定义代理格式无效:\n{{issues}}", + "invalidFormat": "自定义代理格式无效。请确保你的设置遵循正确的 YAML 格式。", + "updateFailed": "更新自定义代理失败:{{error}}", + "deleteFailed": "删除自定义代理失败:{{error}}", + "resetFailed": "重置自定义代理失败:{{error}}", + "agentNotFound": "写入错误:未找到代理", + "noWorkspaceForProject": "未找到项目特定代理的工作区文件夹", + "rulesCleanupFailed": "代理删除成功,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" }, "scope": { "project": "项目", @@ -173,8 +173,8 @@ } }, "marketplace": { - "mode": { - "rulesCleanupFailed": "模式已成功移除,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" + "agent": { + "rulesCleanupFailed": "代理已成功移除,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。" } }, "mdm": { @@ -185,10 +185,10 @@ } }, "prompts": { - "deleteMode": { - "title": "删除自定义模式", - "description": "您确定要删除此 {{scope}} 模式吗?这也将删除位于 {{rulesFolderPath}} 的关联规则文件夹", - "descriptionNoRules": "您确定要删除此自定义模式吗?", + "deleteAgent": { + "title": "删除自定义代理", + "description": "您确定要删除此 {{scope}} 代理吗?这也将删除位于 {{rulesFolderPath}} 的关联规则文件夹", + "descriptionNoRules": "您确定要删除此自定义代理吗?", "confirm": "删除" } }, diff --git a/src/i18n/locales/zh-CN/marketplace.json b/src/i18n/locales/zh-CN/marketplace.json index 50a2ade635..1f8ff99cf0 100644 --- a/src/i18n/locales/zh-CN/marketplace.json +++ b/src/i18n/locales/zh-CN/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "模式", + "agents": "代理", "mcps": "MCP 服务", "match": "匹配" }, "item-card": { - "type-mode": "模式", + "type-agent": "代理", "type-mcp": "MCP 服务", "type-other": "其他", "by-author": "作者:{{author}}", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 1c800d4d37..053774b76a 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -17,7 +17,7 @@ "confirmation": { "reset_state": "您確定要重設擴充套件中的所有狀態和金鑰儲存嗎?此操作無法復原。", "delete_config_profile": "您確定要刪除此設定檔案嗎?", - "delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}" + "delete_custom_agent_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}" }, "errors": { "invalid_data_uri": "資料 URI 格式無效", @@ -89,7 +89,7 @@ "generate_complete_prompt": "Gemini 完成錯誤:{{error}}", "sources": "來源:" }, - "mode_import_failed": "匯入模式失敗:{{error}}" + "agent_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", @@ -109,8 +109,8 @@ "image_saved": "圖片已儲存至 {{path}}", "organization_share_link_copied": "組織分享連結已複製到剪貼簿!", "public_share_link_copied": "公開分享連結已複製到剪貼簿!", - "mode_exported": "模式 '{{mode}}' 已成功匯出", - "mode_imported": "模式已成功匯入" + "agent_exported": "模式 '{{代理}}' 已成功匯出", + "agent_imported": "模式已成功匯入" }, "answers": { "yes": "是", @@ -150,10 +150,10 @@ } } }, - "customModes": { + "customAgents": { "errors": { - "yamlParseError": ".roomodes 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法", - "schemaValidationError": ".roomodes 中自訂模式格式無效:\n{{issues}}", + "yamlParseError": ".rooagents 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法", + "schemaValidationError": ".rooagents 中自訂模式格式無效:\n{{issues}}", "invalidFormat": "自訂模式格式無效。請確保你的設定遵循正確的 YAML 格式。", "updateFailed": "更新自訂模式失敗:{{error}}", "deleteFailed": "刪除自訂模式失敗:{{error}}", @@ -168,7 +168,7 @@ } }, "marketplace": { - "mode": { + "代理": { "rulesCleanupFailed": "模式已成功移除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。" } }, @@ -180,7 +180,7 @@ } }, "prompts": { - "deleteMode": { + "deleteAgent": { "title": "刪除自訂模式", "description": "您確定要刪除此 {{scope}} 模式嗎?這也將刪除位於 {{rulesFolderPath}} 的關聯規則資料夾", "descriptionNoRules": "您確定要刪除此自訂模式嗎?", diff --git a/src/i18n/locales/zh-TW/marketplace.json b/src/i18n/locales/zh-TW/marketplace.json index 3eaeb0e7e0..d6b43ccb8a 100644 --- a/src/i18n/locales/zh-TW/marketplace.json +++ b/src/i18n/locales/zh-TW/marketplace.json @@ -1,11 +1,11 @@ { "type-group": { - "modes": "模式", + "代理": "模式", "mcps": "MCP 伺服器", "match": "符合" }, "item-card": { - "type-mode": "模式", + "type-代理": "模式", "type-mcp": "MCP 伺服器", "type-other": "其他", "by-author": "作者:{{author}}", @@ -23,7 +23,7 @@ "type": { "label": "類型", "all": "所有類型", - "mode": "模式", + "代理": "模式", "mcpServer": "MCP 伺服器" }, "sort": { diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 5c5b9f6d61..43a2617622 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -9,6 +9,7 @@ import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { t } from "../../i18n" import { TelemetryService } from "@roo-code/telemetry" +import type { CustomAgentsManager } from "../../core/config/CustomAgentsManager" import type { CustomModesManager } from "../../core/config/CustomModesManager" export class MarketplaceManager { @@ -17,10 +18,10 @@ export class MarketplaceManager { constructor( private readonly context: vscode.ExtensionContext, - private readonly customModesManager?: CustomModesManager, + private readonly customAgentsManager?: CustomAgentsManager | CustomModesManager, ) { this.configLoader = new RemoteConfigLoader() - this.installer = new SimpleInstaller(context, customModesManager) + this.installer = new SimpleInstaller(context, customAgentsManager) } async getMarketplaceItems(): Promise<{ items: MarketplaceItem[]; errors?: string[] }> { @@ -201,22 +202,40 @@ export class MarketplaceManager { return // No workspace, no project installations } - // Check modes in .roomodes + // Check agents/modes in .rooagents (preferred) or .roomodes (backward compatibility) + const projectAgentsPath = path.join(workspaceFolder.uri.fsPath, ".rooagents") const projectModesPath = path.join(workspaceFolder.uri.fsPath, ".roomodes") + + // Try .rooagents first try { - const content = await fs.readFile(projectModesPath, "utf-8") + const content = await fs.readFile(projectAgentsPath, "utf-8") const data = yaml.parse(content) - if (data?.customModes && Array.isArray(data.customModes)) { - for (const mode of data.customModes) { - if (mode.slug) { - metadata[mode.slug] = { - type: "mode", + if (data?.customAgents && Array.isArray(data.customAgents)) { + for (const agent of data.customAgents) { + if (agent.slug) { + metadata[agent.slug] = { + type: "mode", // Keep as "mode" for marketplace compatibility } } } } } catch (error) { - // File doesn't exist or can't be read, skip + // .rooagents doesn't exist, try .roomodes for backward compatibility + try { + const content = await fs.readFile(projectModesPath, "utf-8") + const data = yaml.parse(content) + if (data?.customModes && Array.isArray(data.customModes)) { + for (const mode of data.customModes) { + if (mode.slug) { + metadata[mode.slug] = { + type: "mode", + } + } + } + } + } catch (error) { + // Neither file exists or can't be read, skip + } } // Check MCPs in .roo/mcp.json diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index be002e2f1d..2ab895c83d 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -5,6 +5,7 @@ import * as yaml from "yaml" import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" +import type { CustomAgentsManager } from "../../core/config/CustomAgentsManager" import type { CustomModesManager } from "../../core/config/CustomModesManager" export interface InstallOptions extends InstallMarketplaceItemOptions { @@ -15,7 +16,7 @@ export interface InstallOptions extends InstallMarketplaceItemOptions { export class SimpleInstaller { constructor( private readonly context: vscode.ExtensionContext, - private readonly customModesManager?: CustomModesManager, + private readonly customAgentsManager?: CustomAgentsManager | CustomModesManager, ) {} async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> { @@ -44,16 +45,16 @@ export class SimpleInstaller { throw new Error("Mode content should not be an array") } - // If CustomModesManager is available, use importModeWithRules - if (this.customModesManager) { + // If CustomAgentsManager is available, use importModeWithRules + if (this.customAgentsManager) { // Transform marketplace content to import format (wrap in customModes array) const importData = { customModes: [yaml.parse(item.content)], } const importYaml = yaml.stringify(importData) - // Call customModesManager.importModeWithRules - const result = await this.customModesManager.importModeWithRules(importYaml, target) + // Call customAgentsManager.importModeWithRules (backward compatible method) + const result = await this.customAgentsManager.importModeWithRules(importYaml, target) if (!result.success) { throw new Error(result.error || "Failed to import mode") @@ -294,7 +295,7 @@ export class SimpleInstaller { } private async removeMode(item: MarketplaceItem, target: "project" | "global"): Promise { - if (!this.customModesManager) { + if (!this.customAgentsManager) { throw new Error("CustomModesManager is not available") } @@ -320,12 +321,12 @@ export class SimpleInstaller { } // Get the current modes to determine the source - const modes = await this.customModesManager.getCustomModes() + const modes = await this.customAgentsManager.getCustomModes() const mode = modes.find((m) => m.slug === modeSlug) - // Use CustomModesManager to delete the mode configuration + // Use CustomAgentsManager to delete the mode configuration // This also handles rules folder deletion - await this.customModesManager.deleteCustomMode(modeSlug, true) + await this.customAgentsManager.deleteCustomMode(modeSlug, true) } private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise { @@ -362,7 +363,18 @@ export class SimpleInstaller { if (!workspaceFolder) { throw new Error("No workspace folder found") } - return path.join(workspaceFolder.uri.fsPath, ".roomodes") + + // Check if .rooagents exists, otherwise use .roomodes for backward compatibility + const rooagentsPath = path.join(workspaceFolder.uri.fsPath, ".rooagents") + const roomodesPath = path.join(workspaceFolder.uri.fsPath, ".roomodes") + + try { + await fs.access(rooagentsPath) + return rooagentsPath + } catch { + // .rooagents doesn't exist, use .roomodes for backward compatibility + return roomodesPath + } } else { const globalSettingsPath = await ensureSettingsDirectoryExists(this.context) return path.join(globalSettingsPath, GlobalFileNames.customModes) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5320190a7b..7de0f13b2e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -4,6 +4,9 @@ import type { ProviderSettings, HistoryItem, ModeConfig, + AgentConfig, + CustomModePrompts, + CustomAgentPrompts, TelemetrySetting, Experiments, ClineMessage, @@ -83,8 +86,12 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" + | "updateCustomAgent" + | "deleteCustomAgent" | "exportModeResult" | "importModeResult" + | "exportAgentResult" + | "importAgentResult" | "checkRulesDirectoryResult" | "deleteCustomModeCheck" | "currentCheckpointUpdated" @@ -170,6 +177,7 @@ export interface ExtensionMessage { listApiConfig?: ProviderSettingsEntry[] mode?: Mode customMode?: ModeConfig + customAgent?: AgentConfig slug?: string success?: boolean values?: Record @@ -295,6 +303,7 @@ export type ExtensionState = Pick< mode: Mode customModes: ModeConfig[] + customAgents?: AgentConfig[] // Optional for backward compatibility toolRequirements?: Record // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled) cwd?: string // Current working directory diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a91d1af7ba..67757d86d8 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -4,6 +4,7 @@ import type { ProviderSettings, PromptComponent, ModeConfig, + AgentConfig, InstallMarketplaceItemOptions, MarketplaceItem, ShareVisibility, @@ -144,8 +145,11 @@ export interface WebviewMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" + | "updateCustomAgent" + | "deleteCustomAgent" | "setopenAiCustomModelInfo" | "openCustomModesSettings" + | "openCustomAgentsSettings" | "checkpointDiff" | "checkpointRestore" | "deleteMcpServer" @@ -199,6 +203,10 @@ export interface WebviewMessage { | "exportModeResult" | "importMode" | "importModeResult" + | "exportAgent" + | "exportAgentResult" + | "importAgent" + | "importAgentResult" | "checkRulesDirectory" | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" @@ -210,7 +218,7 @@ export interface WebviewMessage { | "insertTextIntoTextarea" text?: string editedMessageContent?: string - tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" + tab?: "settings" | "history" | "mcp" | "modes" | "agents" | "chat" | "marketplace" | "account" disabled?: boolean context?: string dataUri?: string @@ -234,6 +242,7 @@ export interface WebviewMessage { setting?: string slug?: string modeConfig?: ModeConfig + agentConfig?: AgentConfig timeout?: number payload?: WebViewMessagePayload source?: "global" | "project" diff --git a/src/shared/modes.ts b/src/shared/modes.ts index f68d25c682..0463ba4580 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -3,11 +3,14 @@ import * as vscode from "vscode" import { type GroupOptions, type GroupEntry, + type AgentConfig, type ModeConfig, + type CustomAgentPrompts, type CustomModePrompts, type ExperimentId, type ToolGroup, type PromptComponent, + DEFAULT_AGENTS, DEFAULT_MODES, } from "@roo-code/types" @@ -16,7 +19,8 @@ import { addCustomInstructions } from "../core/prompts/sections/custom-instructi import { EXPERIMENT_IDS } from "./experiments" import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools" -export type Mode = string +export type Agent = string +export type Mode = Agent // Backward compatibility alias // Helper to extract group name regardless of format export function getGroupName(group: GroupEntry): ToolGroup { @@ -60,94 +64,136 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] { return Array.from(tools) } -// Main modes configuration as an ordered array +// Main agents configuration as an ordered array +export const agents = DEFAULT_AGENTS + +// Main modes configuration as an ordered array (backward compatibility) export const modes = DEFAULT_MODES -// Export the default mode slug -export const defaultModeSlug = modes[0].slug +// Export the default agent slug +export const defaultAgentSlug = agents[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 +// Export the default mode slug (backward compatibility) +export const defaultModeSlug = defaultAgentSlug + +// Helper functions for agents +export function getAgentBySlug(slug: string, customAgents?: AgentConfig[]): AgentConfig | undefined { + // Check custom agents first + const customAgent = customAgents?.find((agent) => agent.slug === slug) + if (customAgent) { + return customAgent } - // Then check built-in modes - return modes.find((mode) => mode.slug === slug) + // Then check built-in agents + return agents.find((agent) => agent.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}`) +export function getAgentConfig(slug: string, customAgents?: AgentConfig[]): AgentConfig { + const agent = getAgentBySlug(slug, customAgents) + if (!agent) { + throw new Error(`No agent found for slug: ${slug}`) } - return mode + return agent } -// Get all available modes, with custom modes overriding built-in modes -export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { - if (!customModes?.length) { - return [...modes] +// Get all available agents, with custom agents overriding built-in agents +export function getAllAgents(customAgents?: AgentConfig[]): AgentConfig[] { + if (!customAgents?.length) { + return [...agents] } - // Start with built-in modes - const allModes = [...modes] + // Start with built-in agents + const allAgents = [...agents] - // Process custom modes - customModes.forEach((customMode) => { - const index = allModes.findIndex((mode) => mode.slug === customMode.slug) + // Process custom agents + customAgents.forEach((customAgent) => { + const index = allAgents.findIndex((agent) => agent.slug === customAgent.slug) if (index !== -1) { - // Override existing mode - allModes[index] = customMode + // Override existing agent + allAgents[index] = customAgent } else { - // Add new mode - allModes.push(customMode) + // Add new agent + allAgents.push(customAgent) } }) - return allModes + return allAgents } -// Check if a mode is custom or an override +// Check if an agent is custom or an override +export function isCustomAgent(slug: string, customAgents?: AgentConfig[]): boolean { + return !!customAgents?.some((agent) => agent.slug === slug) +} + +// Helper functions for modes (backward compatibility) +export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined { + return getAgentBySlug(slug, customModes) +} + +export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig { + return getAgentConfig(slug, customModes) +} + +// Get all available modes, with custom modes overriding built-in modes (backward compatibility) +export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { + return getAllAgents(customModes) +} + +// Check if a mode is custom or an override (backward compatibility) export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean { - return !!customModes?.some((mode) => mode.slug === slug) + return isCustomAgent(slug, customModes) } /** - * Find a mode by its slug, don't fall back to built-in modes + * Find an agent by its slug, don't fall back to built-in agents + */ +export function findAgentBySlug(slug: string, agents: readonly AgentConfig[] | undefined): AgentConfig | undefined { + return agents?.find((agent) => agent.slug === slug) +} + +/** + * Get the agent selection based on the provided agent slug, prompt component, and custom agents. + * If a custom agent is found, it takes precedence over the built-in agents. + * If no custom agent is found, the built-in agent is used with partial merging from promptComponent. + * If neither is found, the default agent is used. + */ +export function getAgentSelection(agent: string, promptComponent?: PromptComponent, customAgents?: AgentConfig[]) { + const customAgent = findAgentBySlug(agent, customAgents) + const builtInAgent = findAgentBySlug(agent, agents) + + // If we have a custom agent, use it entirely + if (customAgent) { + return { + roleDefinition: customAgent.roleDefinition || "", + baseInstructions: customAgent.customInstructions || "", + description: customAgent.description || "", + } + } + + // Otherwise, use built-in agent as base and merge with promptComponent + const baseAgent = builtInAgent || agents[0] // fallback to default agent + + return { + roleDefinition: promptComponent?.roleDefinition || baseAgent.roleDefinition || "", + baseInstructions: promptComponent?.customInstructions || baseAgent.customInstructions || "", + description: baseAgent.description || "", + } +} + +/** + * Find a mode by its slug, don't fall back to built-in modes (backward compatibility) */ export function findModeBySlug(slug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined { - return modes?.find((mode) => mode.slug === slug) + return findAgentBySlug(slug, modes) } /** - * Get the mode selection based on the provided mode slug, prompt component, and custom modes. + * Get the mode selection based on the provided mode slug, prompt component, and custom modes (backward compatibility). * If a custom mode is found, it takes precedence over the built-in modes. * If no custom mode is found, the built-in mode is used with partial merging from promptComponent. * If neither is found, the default mode is used. */ export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) { - const customMode = findModeBySlug(mode, customModes) - const builtInMode = findModeBySlug(mode, modes) - - // If we have a custom mode, use it entirely - if (customMode) { - return { - roleDefinition: customMode.roleDefinition || "", - baseInstructions: customMode.customInstructions || "", - description: customMode.description || "", - } - } - - // Otherwise, use built-in mode as base and merge with promptComponent - const baseMode = builtInMode || modes[0] // fallback to default mode - - return { - roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition || "", - baseInstructions: promptComponent?.customInstructions || baseMode.customInstructions || "", - description: baseMode.description || "", - } + return getAgentSelection(mode, promptComponent, customModes) } // Edit operation parameters that indicate an actual edit operation @@ -164,10 +210,10 @@ export class FileRestrictionError extends Error { } } -export function isToolAllowedForMode( +export function isToolAllowedForAgent( tool: string, - modeSlug: string, - customModes: ModeConfig[], + agentSlug: string, + customAgents: AgentConfig[], toolRequirements?: Record, toolParams?: Record, // All tool parameters experiments?: Record, @@ -192,13 +238,13 @@ export function isToolAllowedForMode( return false } - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { + const agent = getAgentBySlug(agentSlug, customAgents) + if (!agent) { return false } - // Check if tool is in any of the mode's groups and respects any group options - for (const group of mode.groups) { + // Check if tool is in any of the agent's groups and respects any group options + for (const group of agent.groups) { const groupName = getGroupName(group) const options = getGroupOptions(group) @@ -222,7 +268,7 @@ export function isToolAllowedForMode( // Handle single file path validation if (filePath && isEditOperation && !doesFileMatchRegex(filePath, options.fileRegex)) { - throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath, tool) + throw new FileRestrictionError(agent.name, options.fileRegex, options.description, filePath, tool) } // Handle XML args parameter (used by MULTI_FILE_APPLY_DIFF experiment) @@ -240,7 +286,7 @@ export function isToolAllowedForMode( if (extractedPath && !extractedPath.includes("<") && !extractedPath.includes(">")) { if (!doesFileMatchRegex(extractedPath, options.fileRegex)) { throw new FileRestrictionError( - mode.name, + agent.name, options.fileRegex, options.description, extractedPath, @@ -268,37 +314,147 @@ export function isToolAllowedForMode( return false } -// Create the mode-specific default prompts -export const defaultPrompts: Readonly = Object.freeze( +// Backward compatibility function for modes +export function isToolAllowedForMode( + tool: string, + modeSlug: string, + customModes: ModeConfig[], + toolRequirements?: Record, + toolParams?: Record, // All tool parameters + experiments?: Record, +): boolean { + return isToolAllowedForAgent(tool, modeSlug, customModes, toolRequirements, toolParams, experiments) +} + +// Create the agent-specific default prompts +export const defaultAgentPrompts: Readonly = Object.freeze( Object.fromEntries( - modes.map((mode) => [ - mode.slug, + agents.map((agent) => [ + agent.slug, { - roleDefinition: mode.roleDefinition, - whenToUse: mode.whenToUse, - customInstructions: mode.customInstructions, - description: mode.description, + roleDefinition: agent.roleDefinition, + whenToUse: agent.whenToUse, + customInstructions: agent.customInstructions, + description: agent.description, }, ]), ), ) -// Helper function to get all modes with their prompt overrides from extension state -export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise { - const customModes = (await context.globalState.get("customModes")) || [] - const customModePrompts = (await context.globalState.get("customModePrompts")) || {} +// Create the mode-specific default prompts (backward compatibility) +export const defaultPrompts: Readonly = defaultAgentPrompts - const allModes = getAllModes(customModes) - return allModes.map((mode) => ({ - ...mode, - roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition, - whenToUse: customModePrompts[mode.slug]?.whenToUse ?? mode.whenToUse, - customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions, - // description is not overridable via customModePrompts, so we keep the original +// Helper function to get all agents with their prompt overrides from extension state +export async function getAllAgentsWithPrompts(context: vscode.ExtensionContext): Promise { + const customAgents = + (await context.globalState.get("customAgents")) || + (await context.globalState.get("customModes")) || + [] // Fallback for backward compatibility + const customAgentPrompts = + (await context.globalState.get("customAgentPrompts")) || + (await context.globalState.get("customModePrompts")) || + {} // Fallback for backward compatibility + + const allAgents = getAllAgents(customAgents) + return allAgents.map((agent) => ({ + ...agent, + roleDefinition: customAgentPrompts[agent.slug]?.roleDefinition ?? agent.roleDefinition, + whenToUse: customAgentPrompts[agent.slug]?.whenToUse ?? agent.whenToUse, + customInstructions: customAgentPrompts[agent.slug]?.customInstructions ?? agent.customInstructions, + // description is not overridable via customAgentPrompts, so we keep the original })) } -// Helper function to get complete mode details with all overrides +// Helper function to get complete agent details with all overrides +export async function getFullAgentDetails( + agentSlug: string, + customAgents?: AgentConfig[], + customAgentPrompts?: CustomAgentPrompts, + options?: { + cwd?: string + globalCustomInstructions?: string + language?: string + }, +): Promise { + // First get the base agent config from custom agents or built-in agents + const baseAgent = getAgentBySlug(agentSlug, customAgents) || agents.find((a) => a.slug === agentSlug) || agents[0] + + // Check for any prompt component overrides + const promptComponent = customAgentPrompts?.[agentSlug] + + // Get the base custom instructions + const baseCustomInstructions = promptComponent?.customInstructions || baseAgent.customInstructions || "" + const baseWhenToUse = promptComponent?.whenToUse || baseAgent.whenToUse || "" + const baseDescription = promptComponent?.description || baseAgent.description || "" + + // If we have cwd, load and combine all custom instructions + let fullCustomInstructions = baseCustomInstructions + if (options?.cwd) { + fullCustomInstructions = await addCustomInstructions( + baseCustomInstructions, + options.globalCustomInstructions || "", + options.cwd, + agentSlug, + { language: options.language }, + ) + } + + // Return agent with any overrides applied + return { + ...baseAgent, + roleDefinition: promptComponent?.roleDefinition || baseAgent.roleDefinition, + whenToUse: baseWhenToUse, + description: baseDescription, + customInstructions: fullCustomInstructions, + } +} + +// Helper function to safely get agent role definition +export function getAgentRoleDefinition(agentSlug: string, customAgents?: AgentConfig[]): string { + const agent = getAgentBySlug(agentSlug, customAgents) + if (!agent) { + console.warn(`No agent found for slug: ${agentSlug}`) + return "" + } + return agent.roleDefinition +} + +// Helper function to safely get agent description +export function getAgentDescription(agentSlug: string, customAgents?: AgentConfig[]): string { + const agent = getAgentBySlug(agentSlug, customAgents) + if (!agent) { + console.warn(`No agent found for slug: ${agentSlug}`) + return "" + } + return agent.description ?? "" +} + +// Helper function to safely get agent whenToUse +export function getAgentWhenToUse(agentSlug: string, customAgents?: AgentConfig[]): string { + const agent = getAgentBySlug(agentSlug, customAgents) + if (!agent) { + console.warn(`No agent found for slug: ${agentSlug}`) + return "" + } + return agent.whenToUse ?? "" +} + +// Helper function to safely get agent custom instructions +export function getAgentCustomInstructions(agentSlug: string, customAgents?: AgentConfig[]): string { + const agent = getAgentBySlug(agentSlug, customAgents) + if (!agent) { + console.warn(`No agent found for slug: ${agentSlug}`) + return "" + } + return agent.customInstructions ?? "" +} + +// Helper function to get all modes with their prompt overrides from extension state (backward compatibility) +export async function getAllModesWithPrompts(context: vscode.ExtensionContext): Promise { + return getAllAgentsWithPrompts(context) +} + +// Helper function to get complete mode details with all overrides (backward compatibility) export async function getFullModeDetails( modeSlug: string, customModes?: ModeConfig[], @@ -309,75 +465,25 @@ export async function getFullModeDetails( language?: string }, ): Promise { - // First get the base mode config from custom modes or built-in modes - const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0] - - // Check for any prompt component overrides - const promptComponent = customModePrompts?.[modeSlug] - - // Get the base custom instructions - const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" - const baseWhenToUse = promptComponent?.whenToUse || baseMode.whenToUse || "" - const baseDescription = promptComponent?.description || baseMode.description || "" - - // If we have cwd, load and combine all custom instructions - let fullCustomInstructions = baseCustomInstructions - if (options?.cwd) { - fullCustomInstructions = await addCustomInstructions( - baseCustomInstructions, - options.globalCustomInstructions || "", - options.cwd, - modeSlug, - { language: options.language }, - ) - } - - // Return mode with any overrides applied - return { - ...baseMode, - roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition, - whenToUse: baseWhenToUse, - description: baseDescription, - customInstructions: fullCustomInstructions, - } + return getFullAgentDetails(modeSlug, customModes, customModePrompts, options) } -// Helper function to safely get role definition +// Helper function to safely get role definition (backward compatibility) export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string { - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { - console.warn(`No mode found for slug: ${modeSlug}`) - return "" - } - return mode.roleDefinition + return getAgentRoleDefinition(modeSlug, customModes) } -// Helper function to safely get description +// Helper function to safely get description (backward compatibility) export function getDescription(modeSlug: string, customModes?: ModeConfig[]): string { - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { - console.warn(`No mode found for slug: ${modeSlug}`) - return "" - } - return mode.description ?? "" + return getAgentDescription(modeSlug, customModes) } -// Helper function to safely get whenToUse +// Helper function to safely get whenToUse (backward compatibility) export function getWhenToUse(modeSlug: string, customModes?: ModeConfig[]): string { - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { - console.warn(`No mode found for slug: ${modeSlug}`) - return "" - } - return mode.whenToUse ?? "" + return getAgentWhenToUse(modeSlug, customModes) } -// Helper function to safely get custom instructions +// Helper function to safely get custom instructions (backward compatibility) export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig[]): string { - const mode = getModeBySlug(modeSlug, customModes) - if (!mode) { - console.warn(`No mode found for slug: ${modeSlug}`) - return "" - } - return mode.customInstructions ?? "" + return getAgentCustomInstructions(modeSlug, customModes) } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3782242707..77eb714022 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -17,7 +17,7 @@ import SettingsView, { SettingsViewRef } from "./components/settings/SettingsVie import WelcomeView from "./components/welcome/WelcomeView" import McpView from "./components/mcp/McpView" import { MarketplaceView } from "./components/marketplace/MarketplaceView" -import ModesView from "./components/modes/ModesView" +import AgentsView from "./components/agents/AgentsView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import ErrorBoundary from "./components/ErrorBoundary" @@ -26,7 +26,7 @@ import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonI import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" -type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" +type Tab = "settings" | "history" | "mcp" | "agents" | "chat" | "marketplace" | "account" interface HumanRelayDialogState { isOpen: boolean @@ -54,7 +54,7 @@ const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog) const tabsByMessageAction: Partial, Tab>> = { chatButtonClicked: "chat", settingsButtonClicked: "settings", - promptsButtonClicked: "modes", + promptsButtonClicked: "agents", mcpButtonClicked: "mcp", historyButtonClicked: "history", marketplaceButtonClicked: "marketplace", @@ -233,7 +233,7 @@ const App = () => { ) : ( <> - {tab === "modes" && switchTab("chat")} />} + {tab === "agents" && switchTab("chat")} />} {tab === "mcp" && switchTab("chat")} />} {tab === "history" && switchTab("chat")} />} {tab === "settings" && ( diff --git a/webview-ui/src/components/agents/AgentsView.tsx b/webview-ui/src/components/agents/AgentsView.tsx new file mode 100644 index 0000000000..e203373a33 --- /dev/null +++ b/webview-ui/src/components/agents/AgentsView.tsx @@ -0,0 +1,1653 @@ +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react" +import { + VSCodeCheckbox, + VSCodeRadioGroup, + VSCodeRadio, + VSCodeTextArea, + VSCodeLink, + VSCodeTextField, +} from "@vscode/webview-ui-toolkit/react" +import { Trans } from "react-i18next" +import { ChevronDown, X, Upload, Download } from "lucide-react" + +import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types" + +import { + Mode, + getRoleDefinition, + getWhenToUse, + getDescription, + getCustomInstructions, + getAllModes, + findModeBySlug as findCustomModeBySlug, +} from "@roo/modes" +import { TOOL_GROUPS } from "@roo/tools" + +import { vscode } from "@src/utils/vscode" +import { buildDocLink } from "@src/utils/docLinks" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { Tab, TabContent, TabHeader } from "@src/components/common/Tab" +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Popover, + PopoverContent, + PopoverTrigger, + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandItem, + CommandGroup, + Input, + StandardTooltip, +} from "@src/components/ui" +import { DeleteAgentDialog } from "@src/components/agents/DeleteAgentDialog" +import { useEscapeKey } from "@src/hooks/useEscapeKey" + +// Get all available groups that should show in prompts view +const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) + +type AgentSource = "global" | "project" + +type AgentsViewProps = { + onDone: () => void +} + +// Helper to get group name regardless of format +function getGroupName(group: GroupEntry): ToolGroup { + return Array.isArray(group) ? group[0] : group +} + +const AgentsView = ({ onDone }: AgentsViewProps) => { + const { t } = useAppTranslation() + + const { + customModePrompts, + listApiConfigMeta, + currentApiConfigName, + mode, + customInstructions, + setCustomInstructions, + customModes, + } = useExtensionState() + + // Use a local state to track the visually active mode + // This prevents flickering when switching modes rapidly by: + // 1. Updating the UI immediately when a mode is clicked + // 2. Not syncing with the backend mode state (which would cause flickering) + // 3. Still sending the mode change to the backend for persistence + const [visualMode, setVisualMode] = useState(mode) + + // Memoize modes to preserve array order + const modes = useMemo(() => getAllModes(customModes), [customModes]) + + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [selectedPromptContent, setSelectedPromptContent] = useState("") + const [selectedPromptTitle, setSelectedPromptTitle] = useState("") + const [isToolsEditMode, setIsToolsEditMode] = useState(false) + const [showConfigMenu, setShowConfigMenu] = useState(false) + const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) + const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false) + const [isExporting, setIsExporting] = useState(false) + const [isImporting, setIsImporting] = useState(false) + const [showImportDialog, setShowImportDialog] = useState(false) + const [hasRulesToExport, setHasRulesToExport] = useState>({}) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [agentToDelete, setAgentToDelete] = useState<{ + slug: string + name: string + source?: string + rulesFolderPath?: string + } | null>(null) + + // State for mode selection popover and search + const [open, setOpen] = useState(false) + const [searchValue, setSearchValue] = useState("") + const searchInputRef = useRef(null) + + // Local state for mode name input to allow visual emptying + const [localModeName, setLocalModeName] = useState("") + const [currentEditingAgentSlug, setCurrentEditingAgentSlug] = useState(null) + + // Direct update functions + const updateAgentPrompt = useCallback( + (mode: Mode, promptData: PromptComponent) => { + const existingPrompt = customModePrompts?.[mode] as PromptComponent + const updatedPrompt = { ...existingPrompt, ...promptData } + + // Only include properties that differ from defaults + if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) { + delete updatedPrompt.roleDefinition + } + if (updatedPrompt.description === getDescription(mode)) { + delete updatedPrompt.description + } + if (updatedPrompt.whenToUse === getWhenToUse(mode)) { + delete updatedPrompt.whenToUse + } + + vscode.postMessage({ + type: "updatePrompt", + promptMode: mode, + customPrompt: updatedPrompt, + }) + }, + [customModePrompts], + ) + + const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { + const source = modeConfig.source || "global" + + vscode.postMessage({ + type: "updateCustomMode", + slug, + modeConfig: { + ...modeConfig, + source, // Ensure source is set + }, + }) + }, []) + + // Helper function to find a mode by slug + const findModeBySlug = useCallback( + (searchSlug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined => { + return findCustomModeBySlug(searchSlug, modes) + }, + [], + ) + + const switchMode = useCallback((slug: string) => { + vscode.postMessage({ + type: "mode", + text: slug, + }) + }, []) + + // Handle mode switching with explicit state initialization + const handleModeSwitch = useCallback( + (modeConfig: ModeConfig) => { + if (modeConfig.slug === visualMode) return // Prevent unnecessary updates + + // Immediately update visual state for instant feedback + setVisualMode(modeConfig.slug) + + // Then send the mode change message to the backend + switchMode(modeConfig.slug) + + // Exit tools edit mode when switching modes + setIsToolsEditMode(false) + }, + [visualMode, switchMode], + ) + + // Handler for popover open state change + const onOpenChange = useCallback((open: boolean) => { + setOpen(open) + // Reset search when closing the popover + if (!open) { + setTimeout(() => setSearchValue(""), 100) + } + }, []) + + // Use the shared ESC key handler hook + useEscapeKey(open, () => setOpen(false)) + + // Handler for clearing search input + const onClearSearch = useCallback(() => { + setSearchValue("") + searchInputRef.current?.focus() + }, []) + + // Helper function to get current mode's config + const getCurrentMode = useCallback((): ModeConfig | undefined => { + const findMode = (m: ModeConfig): boolean => m.slug === visualMode + return customModes?.find(findMode) || modes.find(findMode) + }, [visualMode, customModes, modes]) + + // Check if the current mode has rules to export + const checkRulesDirectory = useCallback((slug: string) => { + vscode.postMessage({ + type: "checkRulesDirectory", + slug: slug, + }) + }, []) + + // Check rules directory when mode changes + useEffect(() => { + const currentMode = getCurrentMode() + if (currentMode?.slug && hasRulesToExport[currentMode.slug] === undefined) { + checkRulesDirectory(currentMode.slug) + } + }, [getCurrentMode, checkRulesDirectory, hasRulesToExport]) + + // Reset local name state when agent changes + useEffect(() => { + if (currentEditingAgentSlug && currentEditingAgentSlug !== visualMode) { + setCurrentEditingAgentSlug(null) + setLocalModeName("") + } + }, [visualMode, currentEditingAgentSlug]) + + // Helper function to safely access mode properties + const getModeProperty = ( + mode: ModeConfig | undefined, + property: T, + ): ModeConfig[T] | undefined => { + return mode?.[property] + } + + // State for create agent dialog + const [newAgentName, setNewAgentName] = useState("") + const [newAgentSlug, setNewAgentSlug] = useState("") + const [newAgentDescription, setNewAgentDescription] = useState("") + const [newAgentRoleDefinition, setNewAgentRoleDefinition] = useState("") + const [newAgentWhenToUse, setNewAgentWhenToUse] = useState("") + const [newAgentCustomInstructions, setNewAgentCustomInstructions] = useState("") + const [newAgentGroups, setNewAgentGroups] = useState(availableGroups) + const [newAgentSource, setNewAgentSource] = useState("global") + + // Field-specific error states + const [nameError, setNameError] = useState("") + const [slugError, setSlugError] = useState("") + const [descriptionError, setDescriptionError] = useState("") + const [roleDefinitionError, setRoleDefinitionError] = useState("") + const [groupsError, setGroupsError] = useState("") + + // Helper to reset form state + const resetFormState = useCallback(() => { + // Reset form fields + setNewAgentName("") + setNewAgentSlug("") + setNewAgentDescription("") + setNewAgentGroups(availableGroups) + setNewAgentRoleDefinition("") + setNewAgentWhenToUse("") + setNewAgentCustomInstructions("") + setNewAgentSource("global") + // Reset error states + setNameError("") + setSlugError("") + setDescriptionError("") + setRoleDefinitionError("") + setGroupsError("") + }, []) + + // Reset form fields when dialog opens + useEffect(() => { + if (isCreateModeDialogOpen) { + resetFormState() + } + }, [isCreateModeDialogOpen, resetFormState]) + + // Helper function to generate a unique slug from a name + const generateSlug = useCallback((name: string, attempt = 0): string => { + const baseSlug = name + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "-") + .replace(/^-+|-+$/g, "") + return attempt === 0 ? baseSlug : `${baseSlug}-${attempt}` + }, []) + + // Handler for name changes + const handleNameChange = useCallback( + (name: string) => { + setNewAgentName(name) + setNewAgentSlug(generateSlug(name)) + }, + [generateSlug], + ) + + const handleCreateAgent = useCallback(() => { + // Clear previous errors + setNameError("") + setSlugError("") + setDescriptionError("") + setRoleDefinitionError("") + setGroupsError("") + + const source = newAgentSource + const newAgent: ModeConfig = { + slug: newAgentSlug, + name: newAgentName, + description: newAgentDescription.trim() || undefined, + roleDefinition: newAgentRoleDefinition.trim(), + whenToUse: newAgentWhenToUse.trim() || undefined, + customInstructions: newAgentCustomInstructions.trim() || undefined, + groups: newAgentGroups, + source, + } + + // Validate the agent against the schema + const result = modeConfigSchema.safeParse(newAgent) + + if (!result.success) { + // Map Zod errors to specific fields + result.error.errors.forEach((error) => { + const field = error.path[0] as string + const message = error.message + + switch (field) { + case "name": + setNameError(message) + break + case "slug": + setSlugError(message) + break + case "description": + setDescriptionError(message) + break + case "roleDefinition": + setRoleDefinitionError(message) + break + case "groups": + setGroupsError(message) + break + } + }) + return + } + + updateCustomMode(newAgentSlug, newAgent) + switchMode(newAgentSlug) + setIsCreateModeDialogOpen(false) + resetFormState() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + newAgentName, + newAgentSlug, + newAgentDescription, + newAgentRoleDefinition, + newAgentWhenToUse, // Add whenToUse dependency + newAgentCustomInstructions, + newAgentGroups, + newAgentSource, + updateCustomMode, + ]) + + const isNameOrSlugTaken = useCallback( + (name: string, slug: string) => { + return modes.some((m) => m.slug === slug || m.name === name) + }, + [modes], + ) + + const openCreateAgentDialog = useCallback(() => { + const baseNamePrefix = "New Custom Agent" + // Find unique name and slug + let attempt = 0 + let name = baseNamePrefix + let slug = generateSlug(name) + while (isNameOrSlugTaken(name, slug)) { + attempt++ + name = `${baseNamePrefix} ${attempt + 1}` + slug = generateSlug(name) + } + setNewAgentName(name) + setNewAgentSlug(slug) + setIsCreateModeDialogOpen(true) + }, [generateSlug, isNameOrSlugTaken]) + + // Handler for group checkbox changes + const handleGroupChange = useCallback( + (group: ToolGroup, isCustomMode: boolean, customMode: ModeConfig | undefined) => + (e: Event | React.FormEvent) => { + if (!isCustomMode) return // Prevent changes to built-in modes + const target = (e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement) + const checked = target.checked + const oldGroups = customMode?.groups || [] + let newGroups: GroupEntry[] + if (checked) { + newGroups = [...oldGroups, group] + } else { + newGroups = oldGroups.filter((g) => getGroupName(g) !== group) + } + if (customMode) { + const source = customMode.source || "global" + + updateCustomMode(customMode.slug, { + ...customMode, + groups: newGroups, + source, + }) + } + }, + [updateCustomMode], + ) + + // Handle clicks outside the config menu + useEffect(() => { + const handleClickOutside = () => { + if (showConfigMenu) { + setShowConfigMenu(false) + } + } + + document.addEventListener("click", handleClickOutside) + return () => document.removeEventListener("click", handleClickOutside) + }, [showConfigMenu]) + + // Use a ref to store the current agentToDelete value + const agentToDeleteRef = useRef(agentToDelete) + + // Update the ref whenever agentToDelete changes + useEffect(() => { + agentToDeleteRef.current = agentToDelete + }, [agentToDelete]) + + useEffect(() => { + const handler = (event: MessageEvent) => { + const message = event.data + if (message.type === "systemPrompt") { + if (message.text) { + setSelectedPromptContent(message.text) + setSelectedPromptTitle(`System Prompt (${message.mode} mode)`) + setIsDialogOpen(true) + } + } else if (message.type === "exportModeResult") { + setIsExporting(false) + + if (!message.success) { + // Show error message + console.error("Failed to export mode:", message.error) + } + } else if (message.type === "importModeResult") { + setIsImporting(false) + setShowImportDialog(false) + + if (!message.success) { + // Only log error if it's not a cancellation + if (message.error !== "cancelled") { + console.error("Failed to import mode:", message.error) + } + } + } else if (message.type === "checkRulesDirectoryResult") { + setHasRulesToExport((prev) => ({ + ...prev, + [message.slug]: message.hasContent, + })) + } else if (message.type === "deleteCustomModeCheck") { + // Handle the check response + // Use the ref to get the current agentToDelete value + const currentAgentToDelete = agentToDeleteRef.current + if (message.slug && currentAgentToDelete && currentAgentToDelete.slug === message.slug) { + setAgentToDelete({ + ...currentAgentToDelete, + rulesFolderPath: message.rulesFolderPath, + }) + setShowDeleteConfirm(true) + } + } + } + + window.addEventListener("message", handler) + return () => window.removeEventListener("message", handler) + }, []) // Empty dependency array - only register once + + const handleAgentReset = ( + modeSlug: string, + type: "roleDefinition" | "description" | "whenToUse" | "customInstructions", + ) => { + // Only reset for built-in modes + const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent + const updatedPrompt = { ...existingPrompt } + delete updatedPrompt[type] // Remove the field entirely to ensure it reloads from defaults + + vscode.postMessage({ + type: "updatePrompt", + promptMode: modeSlug, + customPrompt: updatedPrompt, + }) + } + + return ( + + +

{t("prompts:title")}

+ +
+ + +
+
e.stopPropagation()} className="flex justify-between items-center mb-3"> +

{t("prompts:modes.title")}

+
+ + + +
+ + + + {showConfigMenu && ( +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className="absolute top-full right-0 w-[200px] mt-1 bg-vscode-editor-background border border-vscode-input-border rounded shadow-md z-[1000]"> +
{ + e.preventDefault() // Prevent blur + vscode.postMessage({ + type: "openCustomModesSettings", + }) + setShowConfigMenu(false) + }} + onClick={(e) => e.preventDefault()}> + {t("prompts:modes.editGlobalModes")} +
+
{ + e.preventDefault() // Prevent blur + vscode.postMessage({ + type: "openFile", + text: "./.roomodes", + values: { + create: true, + content: JSON.stringify({ customModes: [] }, null, 2), + }, + }) + setShowConfigMenu(false) + }} + onClick={(e) => e.preventDefault()}> + {t("prompts:modes.editProjectModes")} +
+
+ )} +
+ + + +
+
+ +
+ + + + +
+ +
+ + + + + + +
+ + {searchValue.length > 0 && ( +
+ +
+ )} +
+ + + {searchValue && ( +
+ {t("prompts:modes.noMatchFound")} +
+ )} +
+ + {modes + .filter((modeConfig) => + searchValue + ? modeConfig.name + .toLowerCase() + .includes(searchValue.toLowerCase()) + : true, + ) + .map((modeConfig) => ( + { + handleModeSwitch(modeConfig) + setOpen(false) + }} + data-testid={`agent-option-${modeConfig.slug}`}> +
+ + {modeConfig.name} + + + {modeConfig.slug} + +
+
+ ))} +
+
+
+
+
+
+ {/* API Configuration - Moved Here */} +
+
{t("prompts:apiConfiguration.title")}
+
+ {t("prompts:apiConfiguration.select")} +
+
+ +
+
+
+ + {/* Name section */} +
+ {/* Only show name and delete for custom modes */} + {visualMode && findModeBySlug(visualMode, customModes) && ( +
+
+
{t("prompts:createModeDialog.name.label")}
+
+ { + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + setCurrentEditingAgentSlug(visualMode) + setLocalModeName(customMode.name) + } + }} + onChange={(e) => { + setLocalModeName(e.target.value) + }} + onBlur={() => { + const customMode = findModeBySlug(visualMode, customModes) + if (customMode && localModeName.trim()) { + // Only update if the name is not empty + updateCustomMode(visualMode, { + ...customMode, + name: localModeName, + source: customMode.source || "global", + }) + } + // Clear the editing state + setCurrentEditingAgentSlug(null) + }} + className="w-full" + /> + + + +
+
+
+ )} + + {/* Role Definition section */} +
+
+
{t("prompts:roleDefinition.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:roleDefinition.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return ( + customMode?.roleDefinition ?? + prompt?.roleDefinition ?? + getRoleDefinition(visualMode) + ) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + roleDefinition: value.trim() || "", + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + roleDefinition: value.trim() || undefined, + }) + } + }} + className="w-full" + rows={5} + data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`} + /> +
+ + {/* Description section */} +
+
+
{t("prompts:description.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:description.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.description ?? prompt?.description ?? getDescription(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + description: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + description: value.trim() || undefined, + }) + } + }} + className="w-full" + data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`} + /> +
+ + {/* When to Use section */} +
+
+
{t("prompts:whenToUse.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:whenToUse.description")} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return customMode?.whenToUse ?? prompt?.whenToUse ?? getWhenToUse(visualMode) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + whenToUse: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + updateAgentPrompt(visualMode, { + whenToUse: value.trim() || undefined, + }) + } + }} + className="w-full" + rows={4} + data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`} + /> +
+ + {/* Mode settings */} + <> + {/* Show tools for all modes */} +
+
+
{t("prompts:tools.title")}
+ {findModeBySlug(visualMode, customModes) && ( + + + + )} +
+ {!findModeBySlug(visualMode, customModes) && ( +
+ {t("prompts:tools.builtInModesText")} +
+ )} + {isToolsEditMode && findModeBySlug(visualMode, customModes) ? ( +
+ {availableGroups.map((group) => { + const currentMode = getCurrentMode() + const isCustomMode = findModeBySlug(visualMode, customModes) + const customMode = isCustomMode + const isGroupEnabled = isCustomMode + ? customMode?.groups?.some((g) => getGroupName(g) === group) + : currentMode?.groups?.some((g) => getGroupName(g) === group) + + return ( + + {t(`prompts:tools.toolNames.${group}`)} + {group === "edit" && ( +
+ {t("prompts:tools.allowedFiles")}{" "} + {(() => { + const currentMode = getCurrentMode() + const editGroup = currentMode?.groups?.find( + (g) => + Array.isArray(g) && + g[0] === "edit" && + g[1]?.fileRegex, + ) + if (!Array.isArray(editGroup)) return t("prompts:allFiles") + return ( + editGroup[1].description || + `/${editGroup[1].fileRegex}/` + ) + })()} +
+ )} +
+ ) + })} +
+ ) : ( +
+ {(() => { + const currentMode = getCurrentMode() + const enabledGroups = currentMode?.groups || [] + + // If there are no enabled groups, display translated "None" + if (enabledGroups.length === 0) { + return t("prompts:tools.noTools") + } + + return enabledGroups + .map((group) => { + const groupName = getGroupName(group) + const displayName = t(`prompts:tools.toolNames.${groupName}`) + if (Array.isArray(group) && group[1]?.fileRegex) { + const description = + group[1].description || `/${group[1].fileRegex}/` + return `${displayName} (${description})` + } + return displayName + }) + .join(", ") + })()} +
+ )} +
+ + + {/* Role definition for both built-in and custom modes */} +
+
+
{t("prompts:customInstructions.title")}
+ {!findModeBySlug(visualMode, customModes) && ( + + + + )} +
+
+ {t("prompts:customInstructions.description", { + modeName: getCurrentMode()?.name || "Code", + })} +
+ { + const customMode = findModeBySlug(visualMode, customModes) + const prompt = customModePrompts?.[visualMode] as PromptComponent + return ( + customMode?.customInstructions ?? + prompt?.customInstructions ?? + getCustomInstructions(mode, customModes) + ) + })()} + onChange={(e) => { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const customMode = findModeBySlug(visualMode, customModes) + if (customMode) { + // For custom modes, update the JSON file + updateCustomMode(visualMode, { + ...customMode, + customInstructions: value.trim() || undefined, + source: customMode.source || "global", + }) + } else { + // For built-in modes, update the prompts + const existingPrompt = customModePrompts?.[visualMode] as PromptComponent + updateAgentPrompt(visualMode, { + ...existingPrompt, + customInstructions: value.trim(), + }) + } + }} + rows={10} + className="w-full" + data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} + /> +
+ { + const currentMode = getCurrentMode() + if (!currentMode) return + + // Open or create an empty file + vscode.postMessage({ + type: "openFile", + text: `./.roo/rules-${currentMode.slug}/rules.md`, + values: { + create: true, + content: "", + }, + }) + }} + /> + ), + }} + /> +
+
+
+ +
+
+ + + + +
+ + {/* Export/Import Mode Buttons */} +
+ {/* Export button - visible when any mode is selected */} + {getCurrentMode() && ( + + )} + {/* Import button - always visible */} + +
+ + {/* Advanced Features Disclosure */} +
+ + + {isSystemPromptDisclosureOpen && ( +
+ {/* Override System Prompt Section */} +
+

+ Override System Prompt +

+
+ { + const currentMode = getCurrentMode() + if (!currentMode) return + + vscode.postMessage({ + type: "openFile", + text: `./.roo/system-prompt-${currentMode.slug}`, + values: { + create: true, + content: "", + }, + }) + }} + /> + ), + "1": ( + + ), + "2": , + }} + /> +
+
+
+ )} +
+
+ +
+

{t("prompts:globalCustomInstructions.title")}

+ +
+ + + +
+ { + const value = + (e as unknown as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + setCustomInstructions(value || undefined) + vscode.postMessage({ + type: "customInstructions", + text: value.trim() || undefined, + }) + }} + rows={4} + className="w-full" + data-testid="global-custom-instructions-textarea" + /> +
+ + vscode.postMessage({ + type: "openFile", + text: "./.roo/rules/rules.md", + values: { + create: true, + content: "", + }, + }) + } + /> + ), + }} + /> +
+
+
+ + {isCreateModeDialogOpen && ( +
+
+
+ +

{t("prompts:createModeDialog.title")}

+
+
{t("prompts:createModeDialog.name.label")}
+ { + handleNameChange(e.target.value) + }} + className="w-full" + /> + {nameError && ( +
{nameError}
+ )} +
+
+
{t("prompts:createModeDialog.slug.label")}
+ { + setNewAgentSlug(e.target.value) + }} + className="w-full" + /> +
+ {t("prompts:createModeDialog.slug.description")} +
+ {slugError && ( +
{slugError}
+ )} +
+
+
{t("prompts:createModeDialog.saveLocation.label")}
+
+ {t("prompts:createModeDialog.saveLocation.description")} +
+ ) => { + const target = ((e as CustomEvent)?.detail?.target || + (e.target as HTMLInputElement)) as HTMLInputElement + setNewAgentSource(target.value as AgentSource) + }}> + + {t("prompts:createModeDialog.saveLocation.global.label")} +
+ {t("prompts:createModeDialog.saveLocation.global.description")} +
+
+ + {t("prompts:createModeDialog.saveLocation.project.label")} +
+ {t("prompts:createModeDialog.saveLocation.project.description")} +
+
+
+
+ +
+
+ {t("prompts:createModeDialog.roleDefinition.label")} +
+
+ {t("prompts:createModeDialog.roleDefinition.description")} +
+ { + setNewAgentRoleDefinition((e.target as HTMLTextAreaElement).value) + }} + rows={4} + className="w-full" + /> + {roleDefinitionError && ( +
+ {roleDefinitionError} +
+ )} +
+ +
+
{t("prompts:createModeDialog.description.label")}
+
+ {t("prompts:createModeDialog.description.description")} +
+ { + setNewAgentDescription((e.target as HTMLInputElement).value) + }} + className="w-full" + /> + {descriptionError && ( +
{descriptionError}
+ )} +
+ +
+
{t("prompts:createModeDialog.whenToUse.label")}
+
+ {t("prompts:createModeDialog.whenToUse.description")} +
+ { + setNewAgentWhenToUse((e.target as HTMLTextAreaElement).value) + }} + rows={3} + className="w-full" + /> +
+
+
{t("prompts:createModeDialog.tools.label")}
+
+ {t("prompts:createModeDialog.tools.description")} +
+
+ {availableGroups.map((group) => ( + getGroupName(g) === group)} + onChange={(e: Event | React.FormEvent) => { + const target = + (e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement) + const checked = target.checked + if (checked) { + setNewAgentGroups([...newAgentGroups, group]) + } else { + setNewAgentGroups( + newAgentGroups.filter((g) => getGroupName(g) !== group), + ) + } + }}> + {t(`prompts:tools.toolNames.${group}`)} + + ))} +
+ {groupsError && ( +
{groupsError}
+ )} +
+
+
+ {t("prompts:createModeDialog.customInstructions.label")} +
+
+ {t("prompts:createModeDialog.customInstructions.description")} +
+ { + setNewAgentCustomInstructions((e.target as HTMLTextAreaElement).value) + }} + rows={4} + className="w-full" + /> +
+
+
+ + +
+
+
+ )} + + {isDialogOpen && ( +
+
+
+ +

+ {selectedPromptTitle || + t("prompts:systemPrompt.title", { + modeName: getCurrentMode()?.name || "Code", + })} +

+
+								{selectedPromptContent}
+							
+
+
+ +
+
+
+ )} + + {/* Import Mode Dialog */} + {showImportDialog && ( +
+
+

{t("prompts:modes.importMode")}

+

+ {t("prompts:importMode.selectLevel")} +

+
+ + +
+
+ + +
+
+
+ )} + + {/* Delete Agent Confirmation Dialog */} + { + if (agentToDelete) { + vscode.postMessage({ + type: "deleteCustomMode", + slug: agentToDelete.slug, + }) + setShowDeleteConfirm(false) + setAgentToDelete(null) + } + }} + /> +
+ ) +} + +export default AgentsView diff --git a/webview-ui/src/components/agents/DeleteAgentDialog.tsx b/webview-ui/src/components/agents/DeleteAgentDialog.tsx new file mode 100644 index 0000000000..a085def87a --- /dev/null +++ b/webview-ui/src/components/agents/DeleteAgentDialog.tsx @@ -0,0 +1,61 @@ +import React from "react" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@src/components/ui" + +interface DeleteAgentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + modeToDelete: { + slug: string + name: string + source?: string + rulesFolderPath?: string + } | null + onConfirm: () => void +} + +export const DeleteAgentDialog: React.FC = ({ + open, + onOpenChange, + modeToDelete, + onConfirm, +}) => { + const { t } = useAppTranslation() + + return ( + + + + {t("prompts:deleteAgent.title")} + + {modeToDelete && ( + <> + {t("prompts:deleteAgent.message", { modeName: modeToDelete.name })} + {modeToDelete.rulesFolderPath && ( +
+ {t("prompts:deleteAgent.rulesFolder", { + folderPath: modeToDelete.rulesFolderPath, + })} +
+ )} + + )} +
+
+ + {t("prompts:deleteAgent.cancel")} + {t("prompts:deleteAgent.confirm")} + +
+
+ ) +} diff --git a/webview-ui/src/components/agents/__tests__/AgentsView.spec.tsx b/webview-ui/src/components/agents/__tests__/AgentsView.spec.tsx new file mode 100644 index 0000000000..e42dfba7d4 --- /dev/null +++ b/webview-ui/src/components/agents/__tests__/AgentsView.spec.tsx @@ -0,0 +1,267 @@ +// npx vitest src/components/modes/__tests__/AgentsView.spec.tsx + +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import AgentsView from "../AgentsView" +import { ExtensionStateContext } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +// Mock vscode API +vitest.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vitest.fn(), + }, +})) + +const mockExtensionState = { + customModePrompts: {}, + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + ], + enhancementApiConfigId: "", + setEnhancementApiConfigId: vitest.fn(), + mode: "code", + customModes: [], + customSupportPrompts: [], + currentApiConfigName: "", + customInstructions: "Initial instructions", + setCustomInstructions: vitest.fn(), +} + +const renderPromptsView = (props = {}) => { + const mockOnDone = vitest.fn() + return render( + + + , + ) +} + +Element.prototype.scrollIntoView = vitest.fn() + +describe("PromptsView", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("displays the current mode name in the select trigger", () => { + renderPromptsView({ mode: "code" }) + const selectTrigger = screen.getByTestId("agent-select-trigger") + expect(selectTrigger).toHaveTextContent("Code") + }) + + it("opens the mode selection popover when the trigger is clicked", async () => { + renderPromptsView() + const selectTrigger = screen.getByTestId("agent-select-trigger") + fireEvent.click(selectTrigger) + await waitFor(() => { + expect(selectTrigger).toHaveAttribute("aria-expanded", "true") + }) + }) + + it("filters mode options based on search input", async () => { + renderPromptsView() + const selectTrigger = screen.getByTestId("agent-select-trigger") + fireEvent.click(selectTrigger) + + const searchInput = screen.getByTestId("agent-search-input") + fireEvent.change(searchInput, { target: { value: "ask" } }) + + await waitFor(() => { + expect(screen.getByTestId("agent-option-ask")).toBeInTheDocument() + expect(screen.queryByTestId("agent-option-code")).not.toBeInTheDocument() + expect(screen.queryByTestId("agent-option-architect")).not.toBeInTheDocument() + }) + }) + + it("selects a mode from the dropdown and sends update message", async () => { + renderPromptsView() + const selectTrigger = screen.getByTestId("agent-select-trigger") + fireEvent.click(selectTrigger) + + const askOption = await waitFor(() => screen.getByTestId("agent-option-ask")) + fireEvent.click(askOption) + + expect(mockExtensionState.setEnhancementApiConfigId).not.toHaveBeenCalled() // Ensure this is not called by mode switch + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "mode", + text: "ask", + }) + await waitFor(() => { + expect(selectTrigger).toHaveAttribute("aria-expanded", "false") + }) + }) + + it("handles prompt changes correctly", async () => { + renderPromptsView() + + // Get the textarea + const textarea = await waitFor(() => screen.getByTestId("code-prompt-textarea")) + + // Simulate VSCode TextArea change event + const changeEvent = new CustomEvent("change", { + detail: { + target: { + value: "New prompt value", + }, + }, + }) + + fireEvent(textarea, changeEvent) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updatePrompt", + promptMode: "code", + customPrompt: { roleDefinition: "New prompt value" }, + }) + }) + + it("resets role definition only for built-in modes", async () => { + const customMode = { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + groups: [], + } + + // Test with built-in mode (code) + const { unmount } = render( + + + , + ) + + // Find and click the role definition reset button + const resetButton = screen.getByTestId("role-definition-reset") + expect(resetButton).toBeInTheDocument() + await fireEvent.click(resetButton) + + // Verify it only resets role definition + // When resetting a built-in mode's role definition, the field should be removed entirely + // from the customPrompt object, not set to undefined. + // This allows the default role definition from the built-in mode to be used instead. + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "updatePrompt", + promptMode: "code", + customPrompt: {}, // Empty object because the role definition field is removed entirely + }) + + // Cleanup before testing custom mode + unmount() + + // Test with custom mode + render( + + + , + ) + + // Verify reset button is not present for custom mode + expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument() + }) + + it("description section behavior for different mode types", async () => { + const customMode = { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + description: "Custom description", + groups: [], + } + + // Test with built-in mode (code) - description section should be shown with reset button + const { unmount } = render( + + + , + ) + + // Verify description reset button IS present for built-in modes + // because built-in modes can have their descriptions customized and reset + expect(screen.queryByTestId("description-reset")).toBeInTheDocument() + + // Cleanup before testing custom mode + unmount() + + // Test with custom mode - description section should be shown + render( + + + , + ) + + // Verify description section is present for custom modes + // but reset button is NOT present (since custom modes manage their own descriptions) + expect(screen.queryByTestId("description-reset")).not.toBeInTheDocument() + + // Verify the description text field is present for custom modes + expect(screen.getByTestId("custom-mode-description-textfield")).toBeInTheDocument() + }) + + it("handles clearing custom instructions correctly", async () => { + const setCustomInstructions = vitest.fn() + renderPromptsView({ + ...mockExtensionState, + customInstructions: "Initial instructions", + setCustomInstructions, + }) + + const textarea = screen.getByTestId("global-custom-instructions-textarea") + + // Simulate VSCode TextArea change event with empty value + // We need to simulate both the CustomEvent format and regular event format + // since the component handles both + Object.defineProperty(textarea, "value", { + writable: true, + value: "", + }) + + const changeEvent = new Event("change", { bubbles: true }) + fireEvent(textarea, changeEvent) + + // The component calls setCustomInstructions with value || undefined + // Since empty string is falsy, it should be undefined + expect(setCustomInstructions).toHaveBeenCalledWith(undefined) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "customInstructions", + text: undefined, + }) + }) + + it("closes the mode selection popover when ESC key is pressed", async () => { + renderPromptsView() + const selectTrigger = screen.getByTestId("agent-select-trigger") + + // Open the popover + fireEvent.click(selectTrigger) + await waitFor(() => { + expect(selectTrigger).toHaveAttribute("aria-expanded", "true") + }) + + // Press ESC key + fireEvent.keyDown(window, { key: "Escape" }) + + // Verify popover is closed + await waitFor(() => { + expect(selectTrigger).toHaveAttribute("aria-expanded", "false") + }) + }) + + it("does not close the popover when ESC is pressed while popover is closed", async () => { + renderPromptsView() + const selectTrigger = screen.getByTestId("agent-select-trigger") + + // Ensure popover is closed + expect(selectTrigger).toHaveAttribute("aria-expanded", "false") + + // Press ESC key + fireEvent.keyDown(window, { key: "Escape" }) + + // Verify popover remains closed + expect(selectTrigger).toHaveAttribute("aria-expanded", "false") + }) +}) diff --git a/webview-ui/src/components/chat/AgentSelector.tsx b/webview-ui/src/components/chat/AgentSelector.tsx new file mode 100644 index 0000000000..1fc4e3ad14 --- /dev/null +++ b/webview-ui/src/components/chat/AgentSelector.tsx @@ -0,0 +1,304 @@ +import React from "react" +import { ChevronUp, Check, X } from "lucide-react" +import { cn } from "@/lib/utils" +import { useRooPortal } from "@/components/ui/hooks/useRooPortal" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { IconButton } from "./IconButton" +import { vscode } from "@/utils/vscode" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Mode, getAllModes } from "@roo/modes" +import { ModeConfig, CustomModePrompts } from "@roo-code/types" +import { telemetryClient } from "@/utils/TelemetryClient" +import { TelemetryEventName } from "@roo-code/types" +import { Fzf } from "fzf" + +// Minimum number of modes required to show search functionality +const SEARCH_THRESHOLD = 6 + +interface AgentSelectorProps { + value: Mode + onChange: (value: Mode) => void + disabled?: boolean + title?: string + triggerClassName?: string + modeShortcutText: string + customModes?: ModeConfig[] + customModePrompts?: CustomModePrompts + disableSearch?: boolean +} + +export const AgentSelector = ({ + value, + onChange, + disabled = false, + title = "", + triggerClassName = "", + modeShortcutText, + customModes, + customModePrompts, + disableSearch = false, +}: AgentSelectorProps) => { + const [open, setOpen] = React.useState(false) + const [searchValue, setSearchValue] = React.useState("") + const searchInputRef = React.useRef(null) + const portalContainer = useRooPortal("roo-portal") + const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() + const { t } = useAppTranslation() + + const trackAgentSelectorOpened = React.useCallback(() => { + // Track telemetry every time the agent selector is opened + telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) + + // Track first-time usage for UI purposes + if (!hasOpenedModeSelector) { + setHasOpenedModeSelector(true) + vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) + } + }, [hasOpenedModeSelector, setHasOpenedModeSelector]) + + // Get all modes including custom modes and merge custom prompt descriptions + const modes = React.useMemo(() => { + const allModes = getAllModes(customModes) + return allModes.map((mode) => ({ + ...mode, + description: customModePrompts?.[mode.slug]?.description ?? mode.description, + })) + }, [customModes, customModePrompts]) + + // Find the selected mode + const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + + // Memoize searchable items for fuzzy search with separate name and description search + const nameSearchItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: [mode.name, mode.slug].filter(Boolean).join(" "), + })) + }, [modes]) + + const descriptionSearchItems = React.useMemo(() => { + return modes.map((mode) => ({ + original: mode, + searchStr: mode.description || "", + })) + }, [modes]) + + // Create memoized Fzf instances for name and description searches + const nameFzfInstance = React.useMemo(() => { + return new Fzf(nameSearchItems, { + selector: (item) => item.searchStr, + }) + }, [nameSearchItems]) + + const descriptionFzfInstance = React.useMemo(() => { + return new Fzf(descriptionSearchItems, { + selector: (item) => item.searchStr, + }) + }, [descriptionSearchItems]) + + // Filter modes based on search value using fuzzy search with priority + const filteredModes = React.useMemo(() => { + if (!searchValue) return modes + + // First search in names/slugs + const nameMatches = nameFzfInstance.find(searchValue) + const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug)) + + // Then search in descriptions + const descriptionMatches = descriptionFzfInstance.find(searchValue) + + // Combine results: name matches first, then description matches + const combinedResults = [ + ...nameMatches.map((result) => result.item.original), + ...descriptionMatches + .filter((result) => !nameMatchedModes.has(result.item.original.slug)) + .map((result) => result.item.original), + ] + + return combinedResults + }, [modes, searchValue, nameFzfInstance, descriptionFzfInstance]) + + const onClearSearch = React.useCallback(() => { + setSearchValue("") + searchInputRef.current?.focus() + }, []) + + const handleSelect = React.useCallback( + (modeSlug: string) => { + onChange(modeSlug as Mode) + setOpen(false) + // Clear search after selection + setSearchValue("") + }, + [onChange], + ) + + const onOpenChange = React.useCallback( + (isOpen: boolean) => { + if (isOpen) trackAgentSelectorOpened() + setOpen(isOpen) + // Clear search when closing + if (!isOpen) { + setSearchValue("") + } + }, + [trackAgentSelectorOpened], + ) + + // Auto-focus search input when popover opens + React.useEffect(() => { + if (open && searchInputRef.current) { + searchInputRef.current.focus() + } + }, [open]) + + // Determine if search should be shown + const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD + + // Combine instruction text for tooltip + const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}` + + const trigger = ( + + + {selectedMode?.name || ""} + + ) + + return ( + + {title ? {trigger} : trigger} + + +
+ {/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */} + {showSearch ? ( +
+ setSearchValue(e.target.value)} + placeholder={t("chat:modeSelector.searchPlaceholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="agent-search-input" + /> + {searchValue.length > 0 && ( +
+ +
+ )} +
+ ) : ( +
+

{instructionText}

+
+ )} + + {/* Mode List */} +
+ {filteredModes.length === 0 && searchValue ? ( +
+ {t("chat:modeSelector.noResults")} +
+ ) : ( +
+ {filteredModes.map((mode) => ( +
handleSelect(mode.slug)} + className={cn( + "px-3 py-1.5 text-sm cursor-pointer flex items-center", + "hover:bg-vscode-list-hoverBackground", + mode.slug === value + ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" + : "", + )} + data-testid="agent-selector-item"> +
+
{mode.name}
+ {mode.description && ( +
+ {mode.description} +
+ )} +
+ {mode.slug === value && } +
+ ))} +
+ )} +
+ + {/* Bottom bar with buttons on left and title on right */} +
+
+ { + window.postMessage( + { + type: "action", + action: "marketplaceButtonClicked", + values: { marketplaceTab: "mode" }, + }, + "*", + ) + setOpen(false) + }} + /> + { + vscode.postMessage({ + type: "switchTab", + tab: "agents", + }) + setOpen(false) + }} + /> +
+ + {/* Info icon and title on the right - only show info icon when search bar is visible */} +
+ {showSearch && ( + + + + )} +

+ {t("chat:modeSelector.title")} +

+
+
+
+
+
+ ) +} + +export default AgentSelector diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 1b0ca2e963..6c18be6617 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -22,7 +22,7 @@ import { convertToMentionPath } from "@/utils/path-mentions" import { StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" -import ModeSelector from "./ModeSelector" +import AgentSelector from "./AgentSelector" import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" @@ -31,7 +31,7 @@ import { IndexingStatusBadge } from "./IndexingStatusBadge" import { SlashCommandsPopover } from "./SlashCommandsPopover" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" -import { EditModeControls } from "./EditModeControls" +import { EditAgentControls } from "./EditAgentControls" interface ChatTextAreaProps { inputValue: string @@ -897,9 +897,9 @@ const ChatTextArea = forwardRef( [setMode], ) - // Helper function to render mode selector - const renderModeSelector = () => ( - ( + ( vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) - // Helper function to render non-edit mode controls - const renderNonEditModeControls = () => ( + // Helper function to render non-edit agent controls + const renderNonEditAgentControls = () => (
-
{renderModeSelector()}
+
{renderAgentSelector()}
(
{isEditMode && ( - ( /> )} - {!isEditMode && renderNonEditModeControls()} + {!isEditMode && renderNonEditAgentControls()}
) }, diff --git a/webview-ui/src/components/chat/EditAgentControls.tsx b/webview-ui/src/components/chat/EditAgentControls.tsx new file mode 100644 index 0000000000..0aa2b806ef --- /dev/null +++ b/webview-ui/src/components/chat/EditAgentControls.tsx @@ -0,0 +1,115 @@ +import React from "react" +import { Mode } from "@roo/modes" +import { Button, StandardTooltip } from "@/components/ui" +import { Image, SendHorizontal } from "lucide-react" +import { cn } from "@/lib/utils" +import AgentSelector from "./AgentSelector" +import { useAppTranslation } from "@/i18n/TranslationContext" + +interface EditAgentControlsProps { + mode: Mode + onModeChange: (value: Mode) => void + modeShortcutText: string + customModes: any + customModePrompts: any + onCancel?: () => void + onSend: () => void + onSelectImages: () => void + sendingDisabled: boolean + shouldDisableImages: boolean +} + +export const EditAgentControls: React.FC = ({ + mode, + onModeChange, + modeShortcutText, + customModes, + customModePrompts, + onCancel, + onSend, + onSelectImages, + sendingDisabled, + shouldDisableImages, +}) => { + const { t } = useAppTranslation() + + return ( +
+
+
+ +
+
+
+ + + + + + + +
+
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/AgentSelector.spec.tsx similarity index 73% rename from webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx rename to webview-ui/src/components/chat/__tests__/AgentSelector.spec.tsx index a829168893..ed613a8376 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/AgentSelector.spec.tsx @@ -1,7 +1,7 @@ import React from "react" import { render, screen, fireEvent } from "@/utils/test-utils" import { describe, test, expect, vi } from "vitest" -import ModeSelector from "../ModeSelector" +import AgentSelector from "../AgentSelector" import { Mode } from "@roo/modes" import { ModeConfig } from "@roo-code/types" @@ -14,8 +14,8 @@ vi.mock("@/utils/vscode", () => ({ vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => ({ - hasOpenedModeSelector: false, - setHasOpenedModeSelector: vi.fn(), + hasOpenedAgentSelector: false, + setHasOpenedAgentSelector: vi.fn(), }), })) @@ -46,7 +46,7 @@ vi.mock("@roo/modes", async () => { } }) -describe("ModeSelector", () => { +describe("AgentSelector", () => { test("shows custom description from customModePrompts", () => { const customModePrompts = { code: { @@ -55,7 +55,7 @@ describe("ModeSelector", () => { } render( - { ) // The component should be rendered - expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() + expect(screen.getByTestId("agent-selector-trigger")).toBeInTheDocument() }) test("falls back to default description when no custom prompt", () => { - render() + render() // The component should be rendered - expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() + expect(screen.getByTestId("agent-selector-trigger")).toBeInTheDocument() }) test("shows search bar when there are more than 6 modes", () => { @@ -84,13 +84,13 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render() // Click to open the popover - fireEvent.click(screen.getByTestId("mode-selector-trigger")) + fireEvent.click(screen.getByTestId("agent-selector-trigger")) // Search input should be visible - expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() + expect(screen.getByTestId("agent-search-input")).toBeInTheDocument() // Info icon should be visible expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument() @@ -108,13 +108,13 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render() // Click to open the popover - fireEvent.click(screen.getByTestId("mode-selector-trigger")) + fireEvent.click(screen.getByTestId("agent-selector-trigger")) // Search input should NOT be visible - expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument() + expect(screen.queryByTestId("agent-search-input")).not.toBeInTheDocument() // Info blurb should be visible expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument() @@ -134,17 +134,17 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render() // Click to open the popover - fireEvent.click(screen.getByTestId("mode-selector-trigger")) + fireEvent.click(screen.getByTestId("agent-selector-trigger")) // Type in search - const searchInput = screen.getByTestId("mode-search-input") + const searchInput = screen.getByTestId("agent-search-input") fireEvent.change(searchInput, { target: { value: "Mode 3" } }) // Should show filtered results - const modeItems = screen.getAllByTestId("mode-selector-item") + const modeItems = screen.getAllByTestId("agent-selector-item") expect(modeItems.length).toBeLessThan(7) // Should have filtered some out }) @@ -159,14 +159,19 @@ describe("ModeSelector", () => { })) render( - , + , ) // Click to open the popover - fireEvent.click(screen.getByTestId("mode-selector-trigger")) + fireEvent.click(screen.getByTestId("agent-selector-trigger")) // Search input should NOT be visible even with 10 modes - expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument() + expect(screen.queryByTestId("agent-search-input")).not.toBeInTheDocument() // Info blurb should be visible instead expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument() @@ -187,13 +192,13 @@ describe("ModeSelector", () => { })) // Don't pass disableSearch prop (should default to false) - render() + render() // Click to open the popover - fireEvent.click(screen.getByTestId("mode-selector-trigger")) + fireEvent.click(screen.getByTestId("agent-selector-trigger")) // Search input should be visible - expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() + expect(screen.getByTestId("agent-search-input")).toBeInTheDocument() // Info icon should be visible const infoIcon = document.querySelector(".codicon-info") diff --git a/webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx b/webview-ui/src/components/chat/__tests__/EditAgentControls.spec.tsx similarity index 84% rename from webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx rename to webview-ui/src/components/chat/__tests__/EditAgentControls.spec.tsx index 2b72202b32..700396e734 100644 --- a/webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/EditAgentControls.spec.tsx @@ -1,7 +1,7 @@ import React from "react" import { render, screen, fireEvent } from "@testing-library/react" import { describe, it, expect, vi, beforeEach } from "vitest" -import { EditModeControls } from "../EditModeControls" +import { EditAgentControls } from "../EditAgentControls" import { Mode } from "@roo/modes" // Mock the translation hook @@ -31,7 +31,7 @@ vi.mock("../ModeSelector", () => ({ ), })) -describe("EditModeControls", () => { +describe("EditAgentControls", () => { const defaultProps = { mode: "code" as Mode, onModeChange: vi.fn(), @@ -50,7 +50,7 @@ describe("EditModeControls", () => { }) it("renders all controls correctly", () => { - render() + render() // Check for mode selector expect(screen.getByTitle("chat:selectMode")).toBeInTheDocument() @@ -66,7 +66,7 @@ describe("EditModeControls", () => { }) it("calls onCancel when Cancel button is clicked", () => { - render() + render() const cancelButton = screen.getByText("Cancel") fireEvent.click(cancelButton) @@ -75,7 +75,7 @@ describe("EditModeControls", () => { }) it("calls onSend when send button is clicked", () => { - render() + render() const sendButton = screen.getByLabelText("chat:save.tooltip") fireEvent.click(sendButton) @@ -84,7 +84,7 @@ describe("EditModeControls", () => { }) it("calls onSelectImages when image button is clicked", () => { - render() + render() const imageButton = screen.getByLabelText("chat:addImages") fireEvent.click(imageButton) @@ -93,7 +93,7 @@ describe("EditModeControls", () => { }) it("disables buttons when sendingDisabled is true", () => { - render() + render() const cancelButton = screen.getByText("Cancel") const sendButton = screen.getByLabelText("chat:save.tooltip") @@ -103,14 +103,14 @@ describe("EditModeControls", () => { }) it("disables image button when shouldDisableImages is true", () => { - render() + render() const imageButton = screen.getByLabelText("chat:addImages") expect(imageButton).toBeDisabled() }) it("does not call onSelectImages when image button is disabled", () => { - render() + render() const imageButton = screen.getByLabelText("chat:addImages") fireEvent.click(imageButton) @@ -119,7 +119,7 @@ describe("EditModeControls", () => { }) it("does not call onSend when send button is disabled", () => { - render() + render() const sendButton = screen.getByLabelText("chat:save.tooltip") fireEvent.click(sendButton) @@ -128,7 +128,7 @@ describe("EditModeControls", () => { }) it("calls onModeChange when mode is changed", () => { - render() + render() const modeSelector = screen.getByTitle("chat:selectMode") fireEvent.change(modeSelector, { target: { value: "architect" } }) diff --git a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx index b7e9951b0f..54f3886693 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceInstallModal.tsx @@ -194,7 +194,7 @@ export const MarketplaceInstallModal: React.FC = ( setValidationError(null) } - const handlePostInstallAction = (tab: "mcp" | "modes") => { + const handlePostInstallAction = (tab: "mcp" | "agents") => { if (tab === "mcp") { // Navigate to MCP tab window.postMessage( @@ -376,7 +376,7 @@ export const MarketplaceInstallModal: React.FC = ( -