Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
efaaed9
feat: add Issue Fixer Orchestrator mode
MuriloFP Jul 3, 2025
57d3fbe
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 3, 2025
ef61905
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 4, 2025
f5a51c4
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 4, 2025
bcbf329
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 5, 2025
80413c0
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 5, 2025
ab10140
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 7, 2025
39c5cf7
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 7, 2025
00a0b63
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
080b61b
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
7a5ad14
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 8, 2025
2c73ff2
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 9, 2025
05ccf57
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 10, 2025
fdb1f35
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 11, 2025
10ce509
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 14, 2025
ab1f9fc
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 15, 2025
74fd8b4
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 15, 2025
6745c8f
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 16, 2025
faf2ee5
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
b2dadf9
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
f648e4c
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 17, 2025
a6d1e60
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 21, 2025
be90907
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 21, 2025
ed3a077
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 22, 2025
856313f
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 24, 2025
4dd68ea
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 28, 2025
b10fa5e
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 29, 2025
f016d7b
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 30, 2025
23855f2
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 31, 2025
3803c29
Merge branch 'RooCodeInc:main' into main
MuriloFP Jul 31, 2025
0a70720
feat: automatically switch to imported mode after successful import (…
MuriloFP Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface ExportResult {
interface ImportResult {
success: boolean
error?: string
importedModes?: string[] // Slugs of successfully imported modes
}

export class CustomModesManager {
Expand Down Expand Up @@ -786,7 +787,7 @@ export class CustomModesManager {
// This excludes the rules-{slug} folder from the path
const relativePath = path.relative(modeRulesDir, filePath)
// Normalize path to use forward slashes for cross-platform compatibility
const normalizedRelativePath = relativePath.replace(/\\/g, '/')
const normalizedRelativePath = relativePath.replace(/\\/g, "/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style inconsistency - this line uses double quotes while the rest of the file uses single quotes. Consider maintaining consistent quote style:

rulesFiles.push({ relativePath: normalizedRelativePath, content: content.trim() })
}
}
Expand Down Expand Up @@ -949,6 +950,9 @@ export class CustomModesManager {
}
}

// Track successfully imported mode slugs
const importedModes: string[] = []

// Process each mode in the import
for (const importMode of importData.customModes) {
const { rulesFiles, ...modeConfig } = importMode
Expand Down Expand Up @@ -980,12 +984,16 @@ export class CustomModesManager {

// Import rules files (this also handles cleanup of existing rules folders)
await this.importRulesFiles(importMode, rulesFiles || [], source)

// Record the imported mode slug
importedModes.push(importMode.slug)
}

// Refresh the modes after import
await this.refreshMergedState()

return { success: true }
// Return success with imported mode slugs
return { success: true, importedModes }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error("Failed to import mode with rules", { error: errorMessage })
Expand Down
41 changes: 41 additions & 0 deletions src/core/config/__tests__/CustomModesManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,47 @@ describe("CustomModesManager", () => {
const newRulePath = Object.keys(writtenFiles).find((p) => p.includes("new-rule.md"))
expect(writtenFiles[newRulePath!]).toBe("New rule content")
})

it("should return imported mode slugs on successful import", async () => {
const importYaml = yaml.stringify({
customModes: [
{
slug: "mode-one",
name: "Mode One",
roleDefinition: "Role One",
groups: ["read"],
},
{
slug: "mode-two",
name: "Mode Two",
roleDefinition: "Role Two",
groups: ["edit"],
},
],
})

let roomodesContent: any = null
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
if (path === mockSettingsPath) {
return yaml.stringify({ customModes: [] })
}
if (path === mockRoomodes && roomodesContent) {
return yaml.stringify(roomodesContent)
}
throw new Error("File not found")
})
;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => {
if (path === mockRoomodes) {
roomodesContent = yaml.parse(content)
}
return Promise.resolve()
})

const result = await manager.importModeWithRules(importYaml)

expect(result.success).toBe(true)
expect(result.importedModes).toEqual(["mode-one", "mode-two"])
})
})
})

Expand Down
123 changes: 123 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,129 @@ describe("webviewMessageHandler - requestRouterModels", () => {
type: "requestRouterModels",
})

describe("webviewMessageHandler - importMode", () => {
beforeEach(() => {
vi.clearAllMocks()
// Add handleModeSwitch to the mock
mockClineProvider.handleModeSwitch = vi.fn()
// Mock customModesManager.importModeWithRules
mockClineProvider.customModesManager.importModeWithRules = vi.fn()
})

it("should switch to imported mode on successful import", async () => {
const mockImportResult = {
success: true,
importedModes: ["imported-mode"],
}

vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
slug: "imported-mode",
name: "Imported Mode",
roleDefinition: "Imported Role",
groups: ["read"],
},
])

// Mock vscode.window.showOpenDialog
const vscode = await import("vscode")
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)

// Mock fs.readFile
const fs = await import("fs/promises")
vi.mocked(fs.readFile).mockResolvedValue("yaml content")

await webviewMessageHandler(mockClineProvider, {
type: "importMode",
source: "project",
})

// Should switch to the imported mode
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledWith("imported-mode")

// Should send success message with imported modes
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "importModeResult",
success: true,
payload: { importedModes: ["imported-mode"] },
})
})

it("should switch to first mode when multiple modes are imported", async () => {
const mockImportResult = {
success: true,
importedModes: ["mode-one", "mode-two"],
}

vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
slug: "mode-one",
name: "Mode One",
roleDefinition: "Role One",
groups: ["read"],
},
{
slug: "mode-two",
name: "Mode Two",
roleDefinition: "Role Two",
groups: ["edit"],
},
])

// Mock vscode.window.showOpenDialog
const vscode = await import("vscode")
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)

// Mock fs.readFile
const fs = await import("fs/promises")
vi.mocked(fs.readFile).mockResolvedValue("yaml content")

await webviewMessageHandler(mockClineProvider, {
type: "importMode",
source: "project",
})

// Should switch to the first mode only
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledWith("mode-one")
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledTimes(1)
})

it("should handle invalid mode slug gracefully", async () => {
const mockImportResult = {
success: true,
importedModes: ["invalid-mode-that-does-not-exist"],
}

vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([])

// Mock vscode.window.showOpenDialog
const vscode = await import("vscode")
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)

// Mock fs.readFile
const fs = await import("fs/promises")
vi.mocked(fs.readFile).mockResolvedValue("yaml content")

await webviewMessageHandler(mockClineProvider, {
type: "importMode",
source: "project",
})

// Should not call handleModeSwitch with invalid mode
expect(mockClineProvider.handleModeSwitch).not.toHaveBeenCalled()

// Should still send success message
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "importModeResult",
success: true,
payload: { importedModes: ["invalid-mode-that-does-not-exist"] },
})
})
})

// Verify successful providers are included
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "routerModels",
Expand Down
40 changes: 38 additions & 2 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ProviderSettings,
type GlobalState,
type ClineMessage,
type ModeConfig,
TelemetryEventName,
} from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
Expand Down Expand Up @@ -1876,12 +1877,47 @@ export const webviewMessageHandler = async (
// Update state after importing
const customModes = await provider.customModesManager.getCustomModes()
await updateGlobalState("customModes", customModes)
await provider.postStateToWebview()

// Send success message to webview
// Switch to the first imported mode if any were imported
if (result.importedModes && result.importedModes.length > 0) {
try {
// Switch to the first imported mode
const modeToSwitch = result.importedModes[0]

// Validate the mode exists before switching
const allModes = await provider.customModesManager.getCustomModes()
const validMode = allModes.find((m: ModeConfig) => m.slug === modeToSwitch)

if (validMode) {
// Validate that the mode slug is a valid string before type assertion
if (typeof modeToSwitch === "string" && modeToSwitch.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mode slug validation uses type assertion without proper type guards. Consider adding a type guard function for safer validation:

await provider.handleModeSwitch(modeToSwitch as Mode)
// Update the webview with the new mode
await provider.postStateToWebview()

// Track telemetry for automatic mode switch after import
TelemetryService.instance.captureEvent(TelemetryEventName.MODE_SWITCH, {
taskId: provider.getCurrentCline()?.taskId || "no-task",
newMode: modeToSwitch,
trigger: "auto-import",
})
} else {
provider.log(`Invalid mode slug for switching: ${modeToSwitch}`)
}
}
} catch (error) {
provider.log(
`Failed to switch to imported mode: ${error instanceof Error ? error.message : String(error)}`,
)
// Continue with success response - import succeeded even if switch failed
}
}

// Send success message to webview with imported modes info
provider.postMessageToWebview({
type: "importModeResult",
success: true,
payload: { importedModes: result.importedModes },
})

// Show success message
Expand Down
23 changes: 19 additions & 4 deletions webview-ui/src/components/modes/ModesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,24 @@ const ModesView = ({ onDone }: ModesViewProps) => {
setIsImporting(false)
setShowImportDialog(false)

if (!message.success) {
// Extract data from message payload
const { success, error } = message
const importedModes = message.payload?.importedModes

if (success && importedModes && importedModes.length > 0) {
// Find the imported mode configuration
const importedModeSlug = importedModes[0]
const importedMode =
findModeBySlug(importedModeSlug, customModes) || modes.find((m) => m.slug === importedModeSlug)

if (importedMode) {
// Switch to the imported mode
handleModeSwitch(importedMode)
}
} else if (!success) {
// Only log error if it's not a cancellation
if (message.error !== "cancelled") {
console.error("Failed to import mode:", message.error)
if (error !== "cancelled") {
console.error("Failed to import mode:", error)
}
}
} else if (message.type === "checkRulesDirectoryResult") {
Expand All @@ -487,7 +501,8 @@ const ModesView = ({ onDone }: ModesViewProps) => {

window.addEventListener("message", handler)
return () => window.removeEventListener("message", handler)
}, []) // Empty dependency array - only register once
// Re-register handler when dependencies change to ensure it has access to latest values
}, [customModes, findModeBySlug, handleModeSwitch, modes])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component re-registers the message event handler when dependencies change, which could potentially cause race conditions if multiple imports happen quickly. Consider using a more stable event handling pattern, perhaps with a cleanup function that properly removes the previous handler before adding a new one, or using a ref to maintain handler stability.


const handleAgentReset = (
modeSlug: string,
Expand Down
40 changes: 40 additions & 0 deletions webview-ui/src/components/modes/__tests__/ModesView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,44 @@ describe("PromptsView", () => {
// Verify popover remains closed
expect(selectTrigger).toHaveAttribute("aria-expanded", "false")
})

it("should switch to imported mode on successful import", async () => {
const mockVscode = vscode as any
mockVscode.postMessage = vitest.fn()

// Render with custom modes
const customModes = [
{
slug: "new-mode",
name: "New Mode",
roleDefinition: "New mode role",
groups: ["read"],
},
]

const { rerender: _rerender } = renderPromptsView({
mode: "code",
customModes: customModes,
})

// Simulate import success message
await waitFor(() => {
window.postMessage(
{
type: "importModeResult",
success: true,
payload: { importedModes: ["new-mode"] },
},
"*",
)
})

// Wait for the mode switch message to be sent
await waitFor(() => {
expect(mockVscode.postMessage).toHaveBeenCalledWith({
type: "mode",
text: "new-mode",
})
})
})
})