From d77c856776b58910118560cb6600057d1fd69153 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 18 Jul 2025 13:32:23 +0000 Subject: [PATCH] feat: include built-in modes in marketplace alongside API modes - Add built-in modes (architect, code, ask, debug, orchestrator) to marketplace - Merge API modes with built-in modes, allowing API to override built-ins - Fallback to built-in modes when API fails - Add comprehensive tests for built-in modes integration - Fixes issue where marketplace only showed 6 modes instead of all available modes Fixes #5884 --- .../marketplace/RemoteConfigLoader.ts | 70 +++++++-- .../__tests__/RemoteConfigLoader.spec.ts | 143 ++++++++++++++++++ 2 files changed, 202 insertions(+), 11 deletions(-) diff --git a/src/services/marketplace/RemoteConfigLoader.ts b/src/services/marketplace/RemoteConfigLoader.ts index a37f619b4d..ff5064ec14 100644 --- a/src/services/marketplace/RemoteConfigLoader.ts +++ b/src/services/marketplace/RemoteConfigLoader.ts @@ -4,6 +4,7 @@ import { z } from "zod" import { getRooCodeApiUrl } from "@roo-code/cloud" import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types" import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "@roo-code/types" +import { modes } from "../../shared/modes" // Response schemas for YAML API responses const modeMarketplaceResponse = z.object({ @@ -32,24 +33,71 @@ export class RemoteConfigLoader { return items } + /** + * Convert built-in modes to marketplace format + */ + private getBuiltInModes(): MarketplaceItem[] { + return modes.map((mode) => ({ + type: "mode" as const, + id: mode.slug, + name: mode.name, + description: mode.description || mode.whenToUse || "Built-in mode", + author: "Roo Code", + tags: ["built-in", "core"], + content: yaml.stringify({ + slug: mode.slug, + name: mode.name, + roleDefinition: mode.roleDefinition, + whenToUse: mode.whenToUse, + description: mode.description, + groups: mode.groups, + customInstructions: mode.customInstructions, + }), + })) + } + private async fetchModes(): Promise { const cacheKey = "modes" const cached = this.getFromCache(cacheKey) if (cached) return cached - const data = await this.fetchWithRetry(`${this.apiBaseUrl}/api/marketplace/modes`) + let apiModes: MarketplaceItem[] = [] + + try { + const data = await this.fetchWithRetry(`${this.apiBaseUrl}/api/marketplace/modes`) - // Parse and validate YAML response - const yamlData = yaml.parse(data) - const validated = modeMarketplaceResponse.parse(yamlData) + // Parse and validate YAML response + const yamlData = yaml.parse(data) + const validated = modeMarketplaceResponse.parse(yamlData) - const items: MarketplaceItem[] = validated.items.map((item) => ({ - type: "mode" as const, - ...item, - })) + apiModes = validated.items.map((item: any) => ({ + type: "mode" as const, + ...item, + })) + } catch (error) { + console.warn("Failed to fetch modes from API, using built-in modes only:", error) + } - this.setCache(cacheKey, items) - return items + // Get built-in modes + const builtInModes = this.getBuiltInModes() + + // Combine built-in modes with API modes, with API modes taking precedence for duplicates + const allModes = [...builtInModes] + + // Add API modes, replacing any built-in modes with the same ID + apiModes.forEach((apiMode) => { + const existingIndex = allModes.findIndex((mode) => mode.id === apiMode.id) + if (existingIndex !== -1) { + // Replace built-in mode with API version + allModes[existingIndex] = apiMode + } else { + // Add new API mode + allModes.push(apiMode) + } + }) + + this.setCache(cacheKey, allModes) + return allModes } private async fetchMcps(): Promise { @@ -63,7 +111,7 @@ export class RemoteConfigLoader { const yamlData = yaml.parse(data) const validated = mcpMarketplaceResponse.parse(yamlData) - const items: MarketplaceItem[] = validated.items.map((item) => ({ + const items: MarketplaceItem[] = validated.items.map((item: any) => ({ type: "mcp" as const, ...item, })) diff --git a/src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts b/src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts index 61740ab5fb..e9667dcf7d 100644 --- a/src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts +++ b/src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts @@ -13,6 +13,29 @@ vi.mock("@roo-code/cloud", () => ({ getRooCodeApiUrl: () => "https://test.api.com", })) +// Mock the modes import +vi.mock("../../../shared/modes", () => ({ + modes: [ + { + slug: "architect", + name: "🏗️ Architect", + roleDefinition: "You are an architect", + whenToUse: "Use for planning", + description: "Plan and design", + groups: ["read", "edit"], + customInstructions: "Plan first", + }, + { + slug: "code", + name: "💻 Code", + roleDefinition: "You are a coder", + whenToUse: "Use for coding", + description: "Write code", + groups: ["read", "edit", "command"], + }, + ], +})) + describe("RemoteConfigLoader", () => { let loader: RemoteConfigLoader @@ -332,4 +355,124 @@ describe("RemoteConfigLoader", () => { Date.now = originalDateNow }) }) + + describe("built-in modes integration", () => { + it("should include built-in modes when API returns empty", async () => { + const mockModesYaml = `items: []` + const mockMcpsYaml = `items: []` + + mockedAxios.get.mockImplementation((url: string) => { + if (url.includes("/modes")) { + return Promise.resolve({ data: mockModesYaml }) + } + if (url.includes("/mcps")) { + return Promise.resolve({ data: mockMcpsYaml }) + } + return Promise.reject(new Error("Unknown URL")) + }) + + const items = await loader.loadAllItems() + + // Should include 2 built-in modes (architect and code from mock) + expect(items).toHaveLength(2) + expect(items[0]).toEqual({ + type: "mode", + id: "architect", + name: "🏗️ Architect", + description: "Plan and design", + author: "Roo Code", + tags: ["built-in", "core"], + content: expect.stringContaining("slug: architect"), + }) + expect(items[1]).toEqual({ + type: "mode", + id: "code", + name: "💻 Code", + description: "Write code", + author: "Roo Code", + tags: ["built-in", "core"], + content: expect.stringContaining("slug: code"), + }) + }) + + it("should merge API modes with built-in modes", async () => { + const mockModesYaml = `items: + - id: "api-mode" + name: "API Mode" + description: "A mode from API" + content: "test content"` + + const mockMcpsYaml = `items: []` + + mockedAxios.get.mockImplementation((url: string) => { + if (url.includes("/modes")) { + return Promise.resolve({ data: mockModesYaml }) + } + if (url.includes("/mcps")) { + return Promise.resolve({ data: mockMcpsYaml }) + } + return Promise.reject(new Error("Unknown URL")) + }) + + const items = await loader.loadAllItems() + + // Should include 2 built-in modes + 1 API mode = 3 total + expect(items).toHaveLength(3) + + // Check that we have both built-in and API modes + const modeIds = items.map(item => item.id) + expect(modeIds).toContain("architect") + expect(modeIds).toContain("code") + expect(modeIds).toContain("api-mode") + }) + + it("should allow API modes to override built-in modes", async () => { + const mockModesYaml = `items: + - id: "architect" + name: "Custom Architect" + description: "Overridden architect mode" + content: "custom content"` + + const mockMcpsYaml = `items: []` + + mockedAxios.get.mockImplementation((url: string) => { + if (url.includes("/modes")) { + return Promise.resolve({ data: mockModesYaml }) + } + if (url.includes("/mcps")) { + return Promise.resolve({ data: mockMcpsYaml }) + } + return Promise.reject(new Error("Unknown URL")) + }) + + const items = await loader.loadAllItems() + + // Should have 2 modes: overridden architect + built-in code + expect(items).toHaveLength(2) + + const architectMode = items.find(item => item.id === "architect") + expect(architectMode).toEqual({ + type: "mode", + id: "architect", + name: "Custom Architect", + description: "Overridden architect mode", + content: "custom content", + }) + + const codeMode = items.find(item => item.id === "code") + expect(codeMode?.name).toBe("💻 Code") // Should be built-in version + }) + + it("should fallback to built-in modes when API fails", async () => { + // Mock API to fail + mockedAxios.get.mockRejectedValue(new Error("API failure")) + + const items = await loader.loadAllItems() + + // Should still return built-in modes + expect(items).toHaveLength(2) + expect(items[0].id).toBe("architect") + expect(items[1].id).toBe("code") + }) + }) })