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
This commit is contained in:
Roo Code 2025-07-18 13:32:23 +00:00
parent 38d8edf05a
commit d77c856776
2 changed files with 202 additions and 11 deletions

View file

@ -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<MarketplaceItem[]> {
const cacheKey = "modes"
const cached = this.getFromCache(cacheKey)
if (cached) return cached
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
let apiModes: MarketplaceItem[] = []
try {
const data = await this.fetchWithRetry<string>(`${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<MarketplaceItem[]> {
@ -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,
}))

View file

@ -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")
})
})
})