Skip to content

Commit 60f08cb

Browse files
committed
fix: resolve linting errors in ModesView and test file
1 parent 3803c29 commit 60f08cb

File tree

6 files changed

+303
-10
lines changed

6 files changed

+303
-10
lines changed

src/core/config/CustomModesManager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface ExportResult {
4141
interface ImportResult {
4242
success: boolean
4343
error?: string
44+
importedModes?: string[] // Slugs of successfully imported modes
4445
}
4546

4647
export class CustomModesManager {
@@ -786,7 +787,7 @@ export class CustomModesManager {
786787
// This excludes the rules-{slug} folder from the path
787788
const relativePath = path.relative(modeRulesDir, filePath)
788789
// Normalize path to use forward slashes for cross-platform compatibility
789-
const normalizedRelativePath = relativePath.replace(/\\/g, '/')
790+
const normalizedRelativePath = relativePath.replace(/\\/g, "/")
790791
rulesFiles.push({ relativePath: normalizedRelativePath, content: content.trim() })
791792
}
792793
}
@@ -949,6 +950,9 @@ export class CustomModesManager {
949950
}
950951
}
951952

953+
// Track successfully imported mode slugs
954+
const importedModes: string[] = []
955+
952956
// Process each mode in the import
953957
for (const importMode of importData.customModes) {
954958
const { rulesFiles, ...modeConfig } = importMode
@@ -980,12 +984,16 @@ export class CustomModesManager {
980984

981985
// Import rules files (this also handles cleanup of existing rules folders)
982986
await this.importRulesFiles(importMode, rulesFiles || [], source)
987+
988+
// Record the imported mode slug
989+
importedModes.push(importMode.slug)
983990
}
984991

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

988-
return { success: true }
995+
// Return success with imported mode slugs
996+
return { success: true, importedModes }
989997
} catch (error) {
990998
const errorMessage = error instanceof Error ? error.message : String(error)
991999
logger.error("Failed to import mode with rules", { error: errorMessage })

src/core/config/__tests__/CustomModesManager.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,47 @@ describe("CustomModesManager", () => {
12351235
const newRulePath = Object.keys(writtenFiles).find((p) => p.includes("new-rule.md"))
12361236
expect(writtenFiles[newRulePath!]).toBe("New rule content")
12371237
})
1238+
1239+
it("should return imported mode slugs on successful import", async () => {
1240+
const importYaml = yaml.stringify({
1241+
customModes: [
1242+
{
1243+
slug: "mode-one",
1244+
name: "Mode One",
1245+
roleDefinition: "Role One",
1246+
groups: ["read"],
1247+
},
1248+
{
1249+
slug: "mode-two",
1250+
name: "Mode Two",
1251+
roleDefinition: "Role Two",
1252+
groups: ["edit"],
1253+
},
1254+
],
1255+
})
1256+
1257+
let roomodesContent: any = null
1258+
;(fs.readFile as Mock).mockImplementation(async (path: string) => {
1259+
if (path === mockSettingsPath) {
1260+
return yaml.stringify({ customModes: [] })
1261+
}
1262+
if (path === mockRoomodes && roomodesContent) {
1263+
return yaml.stringify(roomodesContent)
1264+
}
1265+
throw new Error("File not found")
1266+
})
1267+
;(fs.writeFile as Mock).mockImplementation(async (path: string, content: string) => {
1268+
if (path === mockRoomodes) {
1269+
roomodesContent = yaml.parse(content)
1270+
}
1271+
return Promise.resolve()
1272+
})
1273+
1274+
const result = await manager.importModeWithRules(importYaml)
1275+
1276+
expect(result.success).toBe(true)
1277+
expect(result.importedModes).toEqual(["mode-one", "mode-two"])
1278+
})
12381279
})
12391280
})
12401281

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ vi.mock("vscode", () => ({
4343
window: {
4444
showInformationMessage: vi.fn(),
4545
showErrorMessage: vi.fn(),
46+
showOpenDialog: vi.fn(),
4647
},
4748
workspace: {
4849
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
4950
},
51+
Uri: {
52+
file: vi.fn((path: string) => ({ fsPath: path })),
53+
},
5054
}))
5155

5256
vi.mock("../../../i18n", () => ({
@@ -70,14 +74,17 @@ vi.mock("../../../i18n", () => ({
7074
vi.mock("fs/promises", () => {
7175
const mockRm = vi.fn().mockResolvedValue(undefined)
7276
const mockMkdir = vi.fn().mockResolvedValue(undefined)
77+
const mockReadFile = vi.fn().mockResolvedValue("")
7378

7479
return {
7580
default: {
7681
rm: mockRm,
7782
mkdir: mockMkdir,
83+
readFile: mockReadFile,
7884
},
7985
rm: mockRm,
8086
mkdir: mockMkdir,
87+
readFile: mockReadFile,
8188
}
8289
})
8390

@@ -374,6 +381,167 @@ describe("webviewMessageHandler - requestRouterModels", () => {
374381
})
375382
})
376383

384+
describe("webviewMessageHandler - importMode", () => {
385+
beforeEach(() => {
386+
vi.clearAllMocks()
387+
// Add handleModeSwitch to the mock
388+
mockClineProvider.handleModeSwitch = vi.fn()
389+
// Cast to any to add mock methods
390+
;(mockClineProvider.customModesManager as any).importModeWithRules = vi.fn()
391+
})
392+
393+
it("should switch to imported mode after successful import", async () => {
394+
const mockImportResult = {
395+
success: true,
396+
importedModes: ["imported-mode"],
397+
}
398+
399+
vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
400+
// Mock getCustomModes to return the imported mode so validation passes
401+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
402+
{
403+
name: "Imported Mode",
404+
slug: "imported-mode",
405+
roleDefinition: "Test Role",
406+
groups: [],
407+
source: "project",
408+
} as ModeConfig,
409+
])
410+
411+
// Mock vscode.window.showOpenDialog
412+
const vscode = await import("vscode")
413+
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)
414+
415+
// Mock fs.readFile
416+
const fs = await import("fs/promises")
417+
vi.mocked(fs.readFile).mockResolvedValue("yaml content")
418+
419+
await webviewMessageHandler(mockClineProvider, {
420+
type: "importMode",
421+
source: "project",
422+
})
423+
424+
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledWith("imported-mode")
425+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
426+
type: "importModeResult",
427+
success: true,
428+
importedModes: ["imported-mode"],
429+
})
430+
})
431+
432+
it("should not switch mode if import fails", async () => {
433+
const mockImportResult = {
434+
success: false,
435+
error: "Import failed",
436+
}
437+
438+
vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
439+
440+
// Mock vscode.window.showOpenDialog
441+
const vscode = await import("vscode")
442+
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)
443+
444+
// Mock fs.readFile
445+
const fs = await import("fs/promises")
446+
vi.mocked(fs.readFile).mockResolvedValue("yaml content")
447+
448+
await webviewMessageHandler(mockClineProvider, {
449+
type: "importMode",
450+
source: "project",
451+
})
452+
453+
expect(mockClineProvider.handleModeSwitch).not.toHaveBeenCalled()
454+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
455+
type: "importModeResult",
456+
success: false,
457+
error: "Import failed",
458+
})
459+
})
460+
461+
it("should switch to first mode when multiple modes are imported", async () => {
462+
const mockImportResult = {
463+
success: true,
464+
importedModes: ["mode-one", "mode-two", "mode-three"],
465+
}
466+
467+
vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
468+
// Mock getCustomModes to return the imported modes so validation passes
469+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
470+
{
471+
name: "Mode One",
472+
slug: "mode-one",
473+
roleDefinition: "Test Role",
474+
groups: [],
475+
source: "project",
476+
} as ModeConfig,
477+
{
478+
name: "Mode Two",
479+
slug: "mode-two",
480+
roleDefinition: "Test Role",
481+
groups: [],
482+
source: "project",
483+
} as ModeConfig,
484+
{
485+
name: "Mode Three",
486+
slug: "mode-three",
487+
roleDefinition: "Test Role",
488+
groups: [],
489+
source: "project",
490+
} as ModeConfig,
491+
])
492+
493+
// Mock vscode.window.showOpenDialog
494+
const vscode = await import("vscode")
495+
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/modes.yaml" }] as any)
496+
497+
// Mock fs.readFile
498+
const fs = await import("fs/promises")
499+
vi.mocked(fs.readFile).mockResolvedValue("yaml content with multiple modes")
500+
501+
await webviewMessageHandler(mockClineProvider, {
502+
type: "importMode",
503+
source: "project",
504+
})
505+
506+
// Should switch to the first mode only
507+
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledWith("mode-one")
508+
expect(mockClineProvider.handleModeSwitch).toHaveBeenCalledTimes(1)
509+
})
510+
511+
it("should handle invalid mode slug gracefully", async () => {
512+
const mockImportResult = {
513+
success: true,
514+
importedModes: ["invalid-mode-that-does-not-exist"],
515+
}
516+
517+
vi.mocked(mockClineProvider.customModesManager.importModeWithRules).mockResolvedValue(mockImportResult)
518+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([])
519+
520+
// Mock vscode.window.showOpenDialog
521+
const vscode = await import("vscode")
522+
vi.mocked(vscode.window.showOpenDialog).mockResolvedValue([{ fsPath: "/path/to/mode.yaml" }] as any)
523+
524+
// Mock fs.readFile
525+
const fs = await import("fs/promises")
526+
vi.mocked(fs.readFile).mockResolvedValue("yaml content")
527+
528+
await webviewMessageHandler(mockClineProvider, {
529+
type: "importMode",
530+
source: "project",
531+
})
532+
533+
// Should not call handleModeSwitch with invalid mode
534+
expect(mockClineProvider.handleModeSwitch).not.toHaveBeenCalled()
535+
536+
// Should still send success message
537+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
538+
type: "importModeResult",
539+
success: true,
540+
importedModes: ["invalid-mode-that-does-not-exist"],
541+
})
542+
})
543+
})
544+
377545
describe("webviewMessageHandler - deleteCustomMode", () => {
378546
beforeEach(() => {
379547
vi.clearAllMocks()

src/core/webview/webviewMessageHandler.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type ProviderSettings,
1212
type GlobalState,
1313
type ClineMessage,
14+
type ModeConfig,
1415
TelemetryEventName,
1516
} from "@roo-code/types"
1617
import { CloudService } from "@roo-code/cloud"
@@ -25,6 +26,7 @@ import { supportPrompt } from "../../shared/support-prompt"
2526
import { MessageEnhancer } from "./messageEnhancer"
2627

2728
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
29+
import { ExtensionMessage } from "../../shared/ExtensionMessage"
2830
import { checkExistKey } from "../../shared/checkExistApiConfig"
2931
import { experimentDefault } from "../../shared/experiments"
3032
import { Terminal } from "../../integrations/terminal/Terminal"
@@ -1876,13 +1878,34 @@ export const webviewMessageHandler = async (
18761878
// Update state after importing
18771879
const customModes = await provider.customModesManager.getCustomModes()
18781880
await updateGlobalState("customModes", customModes)
1879-
await provider.postStateToWebview()
18801881

1881-
// Send success message to webview
1882+
// Switch to the first imported mode if any were imported
1883+
if (result.importedModes && result.importedModes.length > 0) {
1884+
try {
1885+
// Switch to the first imported mode
1886+
const modeToSwitch = result.importedModes[0]
1887+
1888+
// Validate the mode exists before switching
1889+
const allModes = await provider.customModesManager.getCustomModes()
1890+
const validMode = allModes.find((m: ModeConfig) => m.slug === modeToSwitch)
1891+
1892+
if (validMode) {
1893+
await provider.handleModeSwitch(modeToSwitch as Mode)
1894+
// Update the webview with the new mode
1895+
await provider.postStateToWebview()
1896+
}
1897+
} catch (error) {
1898+
provider.log(`Failed to switch to imported mode: ${error}`)
1899+
// Continue with success response - import succeeded even if switch failed
1900+
}
1901+
}
1902+
1903+
// Send success message to webview with imported modes info
18821904
provider.postMessageToWebview({
18831905
type: "importModeResult",
18841906
success: true,
1885-
})
1907+
importedModes: result.importedModes,
1908+
} as ExtensionMessage)
18861909

18871910
// Show success message
18881911
vscode.window.showInformationMessage(t("common:info.mode_imported"))
@@ -1892,7 +1915,7 @@ export const webviewMessageHandler = async (
18921915
type: "importModeResult",
18931916
success: false,
18941917
error: result.error,
1895-
})
1918+
} as ExtensionMessage)
18961919

18971920
// Show error message
18981921
vscode.window.showErrorMessage(t("common:errors.mode_import_failed", { error: result.error }))

webview-ui/src/components/modes/ModesView.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,23 @@ const ModesView = ({ onDone }: ModesViewProps) => {
460460
setIsImporting(false)
461461
setShowImportDialog(false)
462462

463-
if (!message.success) {
463+
// Extract data directly from message
464+
const { success, importedModes, error } = message
465+
466+
if (success && importedModes && importedModes.length > 0) {
467+
// Find the imported mode configuration
468+
const importedModeSlug = importedModes[0]
469+
const importedMode =
470+
findModeBySlug(importedModeSlug, customModes) || modes.find((m) => m.slug === importedModeSlug)
471+
472+
if (importedMode) {
473+
// Switch to the imported mode
474+
handleModeSwitch(importedMode)
475+
}
476+
} else if (!success) {
464477
// Only log error if it's not a cancellation
465-
if (message.error !== "cancelled") {
466-
console.error("Failed to import mode:", message.error)
478+
if (error !== "cancelled") {
479+
console.error("Failed to import mode:", error)
467480
}
468481
}
469482
} else if (message.type === "checkRulesDirectoryResult") {
@@ -487,7 +500,7 @@ const ModesView = ({ onDone }: ModesViewProps) => {
487500

488501
window.addEventListener("message", handler)
489502
return () => window.removeEventListener("message", handler)
490-
}, []) // Empty dependency array - only register once
503+
}, [customModes, findModeBySlug, handleModeSwitch, modes])
491504

492505
const handleAgentReset = (
493506
modeSlug: string,

0 commit comments

Comments
 (0)