mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
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:
parent
38d8edf05a
commit
d77c856776
2 changed files with 202 additions and 11 deletions
|
|
@ -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,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue