Skip to content

Commit d77c856

Browse files
committed
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
1 parent 38d8edf commit d77c856

File tree

2 files changed

+203
-12
lines changed

2 files changed

+203
-12
lines changed

src/services/marketplace/RemoteConfigLoader.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod"
44
import { getRooCodeApiUrl } from "@roo-code/cloud"
55
import type { MarketplaceItem, MarketplaceItemType } from "@roo-code/types"
66
import { modeMarketplaceItemSchema, mcpMarketplaceItemSchema } from "@roo-code/types"
7+
import { modes } from "../../shared/modes"
78

89
// Response schemas for YAML API responses
910
const modeMarketplaceResponse = z.object({
@@ -32,24 +33,71 @@ export class RemoteConfigLoader {
3233
return items
3334
}
3435

36+
/**
37+
* Convert built-in modes to marketplace format
38+
*/
39+
private getBuiltInModes(): MarketplaceItem[] {
40+
return modes.map((mode) => ({
41+
type: "mode" as const,
42+
id: mode.slug,
43+
name: mode.name,
44+
description: mode.description || mode.whenToUse || "Built-in mode",
45+
author: "Roo Code",
46+
tags: ["built-in", "core"],
47+
content: yaml.stringify({
48+
slug: mode.slug,
49+
name: mode.name,
50+
roleDefinition: mode.roleDefinition,
51+
whenToUse: mode.whenToUse,
52+
description: mode.description,
53+
groups: mode.groups,
54+
customInstructions: mode.customInstructions,
55+
}),
56+
}))
57+
}
58+
3559
private async fetchModes(): Promise<MarketplaceItem[]> {
3660
const cacheKey = "modes"
3761
const cached = this.getFromCache(cacheKey)
3862
if (cached) return cached
3963

40-
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
41-
42-
// Parse and validate YAML response
43-
const yamlData = yaml.parse(data)
44-
const validated = modeMarketplaceResponse.parse(yamlData)
64+
let apiModes: MarketplaceItem[] = []
65+
66+
try {
67+
const data = await this.fetchWithRetry<string>(`${this.apiBaseUrl}/api/marketplace/modes`)
68+
69+
// Parse and validate YAML response
70+
const yamlData = yaml.parse(data)
71+
const validated = modeMarketplaceResponse.parse(yamlData)
72+
73+
apiModes = validated.items.map((item: any) => ({
74+
type: "mode" as const,
75+
...item,
76+
}))
77+
} catch (error) {
78+
console.warn("Failed to fetch modes from API, using built-in modes only:", error)
79+
}
4580

46-
const items: MarketplaceItem[] = validated.items.map((item) => ({
47-
type: "mode" as const,
48-
...item,
49-
}))
81+
// Get built-in modes
82+
const builtInModes = this.getBuiltInModes()
83+
84+
// Combine built-in modes with API modes, with API modes taking precedence for duplicates
85+
const allModes = [...builtInModes]
86+
87+
// Add API modes, replacing any built-in modes with the same ID
88+
apiModes.forEach((apiMode) => {
89+
const existingIndex = allModes.findIndex((mode) => mode.id === apiMode.id)
90+
if (existingIndex !== -1) {
91+
// Replace built-in mode with API version
92+
allModes[existingIndex] = apiMode
93+
} else {
94+
// Add new API mode
95+
allModes.push(apiMode)
96+
}
97+
})
5098

51-
this.setCache(cacheKey, items)
52-
return items
99+
this.setCache(cacheKey, allModes)
100+
return allModes
53101
}
54102

55103
private async fetchMcps(): Promise<MarketplaceItem[]> {
@@ -63,7 +111,7 @@ export class RemoteConfigLoader {
63111
const yamlData = yaml.parse(data)
64112
const validated = mcpMarketplaceResponse.parse(yamlData)
65113

66-
const items: MarketplaceItem[] = validated.items.map((item) => ({
114+
const items: MarketplaceItem[] = validated.items.map((item: any) => ({
67115
type: "mcp" as const,
68116
...item,
69117
}))

src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@ vi.mock("@roo-code/cloud", () => ({
1313
getRooCodeApiUrl: () => "https://test.api.com",
1414
}))
1515

16+
// Mock the modes import
17+
vi.mock("../../../shared/modes", () => ({
18+
modes: [
19+
{
20+
slug: "architect",
21+
name: "🏗️ Architect",
22+
roleDefinition: "You are an architect",
23+
whenToUse: "Use for planning",
24+
description: "Plan and design",
25+
groups: ["read", "edit"],
26+
customInstructions: "Plan first",
27+
},
28+
{
29+
slug: "code",
30+
name: "💻 Code",
31+
roleDefinition: "You are a coder",
32+
whenToUse: "Use for coding",
33+
description: "Write code",
34+
groups: ["read", "edit", "command"],
35+
},
36+
],
37+
}))
38+
1639
describe("RemoteConfigLoader", () => {
1740
let loader: RemoteConfigLoader
1841

@@ -332,4 +355,124 @@ describe("RemoteConfigLoader", () => {
332355
Date.now = originalDateNow
333356
})
334357
})
358+
359+
describe("built-in modes integration", () => {
360+
it("should include built-in modes when API returns empty", async () => {
361+
const mockModesYaml = `items: []`
362+
const mockMcpsYaml = `items: []`
363+
364+
mockedAxios.get.mockImplementation((url: string) => {
365+
if (url.includes("/modes")) {
366+
return Promise.resolve({ data: mockModesYaml })
367+
}
368+
if (url.includes("/mcps")) {
369+
return Promise.resolve({ data: mockMcpsYaml })
370+
}
371+
return Promise.reject(new Error("Unknown URL"))
372+
})
373+
374+
const items = await loader.loadAllItems()
375+
376+
// Should include 2 built-in modes (architect and code from mock)
377+
expect(items).toHaveLength(2)
378+
expect(items[0]).toEqual({
379+
type: "mode",
380+
id: "architect",
381+
name: "🏗️ Architect",
382+
description: "Plan and design",
383+
author: "Roo Code",
384+
tags: ["built-in", "core"],
385+
content: expect.stringContaining("slug: architect"),
386+
})
387+
expect(items[1]).toEqual({
388+
type: "mode",
389+
id: "code",
390+
name: "💻 Code",
391+
description: "Write code",
392+
author: "Roo Code",
393+
tags: ["built-in", "core"],
394+
content: expect.stringContaining("slug: code"),
395+
})
396+
})
397+
398+
it("should merge API modes with built-in modes", async () => {
399+
const mockModesYaml = `items:
400+
- id: "api-mode"
401+
name: "API Mode"
402+
description: "A mode from API"
403+
content: "test content"`
404+
405+
const mockMcpsYaml = `items: []`
406+
407+
mockedAxios.get.mockImplementation((url: string) => {
408+
if (url.includes("/modes")) {
409+
return Promise.resolve({ data: mockModesYaml })
410+
}
411+
if (url.includes("/mcps")) {
412+
return Promise.resolve({ data: mockMcpsYaml })
413+
}
414+
return Promise.reject(new Error("Unknown URL"))
415+
})
416+
417+
const items = await loader.loadAllItems()
418+
419+
// Should include 2 built-in modes + 1 API mode = 3 total
420+
expect(items).toHaveLength(3)
421+
422+
// Check that we have both built-in and API modes
423+
const modeIds = items.map(item => item.id)
424+
expect(modeIds).toContain("architect")
425+
expect(modeIds).toContain("code")
426+
expect(modeIds).toContain("api-mode")
427+
})
428+
429+
it("should allow API modes to override built-in modes", async () => {
430+
const mockModesYaml = `items:
431+
- id: "architect"
432+
name: "Custom Architect"
433+
description: "Overridden architect mode"
434+
content: "custom content"`
435+
436+
const mockMcpsYaml = `items: []`
437+
438+
mockedAxios.get.mockImplementation((url: string) => {
439+
if (url.includes("/modes")) {
440+
return Promise.resolve({ data: mockModesYaml })
441+
}
442+
if (url.includes("/mcps")) {
443+
return Promise.resolve({ data: mockMcpsYaml })
444+
}
445+
return Promise.reject(new Error("Unknown URL"))
446+
})
447+
448+
const items = await loader.loadAllItems()
449+
450+
// Should have 2 modes: overridden architect + built-in code
451+
expect(items).toHaveLength(2)
452+
453+
const architectMode = items.find(item => item.id === "architect")
454+
expect(architectMode).toEqual({
455+
type: "mode",
456+
id: "architect",
457+
name: "Custom Architect",
458+
description: "Overridden architect mode",
459+
content: "custom content",
460+
})
461+
462+
const codeMode = items.find(item => item.id === "code")
463+
expect(codeMode?.name).toBe("💻 Code") // Should be built-in version
464+
})
465+
466+
it("should fallback to built-in modes when API fails", async () => {
467+
// Mock API to fail
468+
mockedAxios.get.mockRejectedValue(new Error("API failure"))
469+
470+
const items = await loader.loadAllItems()
471+
472+
// Should still return built-in modes
473+
expect(items).toHaveLength(2)
474+
expect(items[0].id).toBe("architect")
475+
expect(items[1].id).toBe("code")
476+
})
477+
})
335478
})

0 commit comments

Comments
 (0)