From a2792d1e9f364859cb1d7e46d9904b60afeae44f Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 13:59:11 -0700 Subject: [PATCH 01/10] feat: auto-switch to imported mode after successful import Fixes [#8239](https://github.com/RooCodeInc/Roo-Code/issues/8239) ## Summary Automatically switches to the imported mode after a successful mode import, providing immediate feedback and eliminating the need for manual mode selection. ## Changes ### Backend (CustomModesManager.ts) - Modified `importModeWithRules()` to return the imported mode's slug in the result - Returns `{ success: true, slug: importData.customModes[0]?.slug }` on success - Enables the UI to know which mode was imported for automatic switching ### Message Handler (webviewMessageHandler.ts) - Updated `importMode` case to handle the new slug return value - Passes the slug to the webview via `importModeResult` message - Maintains backward compatibility with existing error handling ### UI (ModesView.tsx) - Added auto-switch logic in `importModeResult` message handler - Attempts to find imported mode in fresh customModes list - Falls back to direct slug-based switch if mode not yet in list - Updates visual mode state for immediate UI feedback - Only switches on success with a valid slug provided ### Tests (ModesView.import-switch.spec.tsx) - Added new test suite for import auto-switch behavior - Tests successful switch when slug is provided - Tests no-switch behavior on import failure or missing slug - Verifies both backend message and UI state updates ## Testing - New test coverage added for auto-switch scenarios - Existing import/export functionality remains unchanged - Works with both global and project-level imports --- src/core/config/CustomModesManager.ts | 4 +- src/core/webview/webviewMessageHandler.ts | 3 +- webview-ui/src/components/modes/ModesView.tsx | 27 +++++- .../ModesView.import-switch.spec.tsx | 94 +++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index a9a2e6a6b55a..3ce96a5fb9b6 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -40,6 +40,7 @@ interface ExportResult { interface ImportResult { success: boolean + slug?: string error?: string } @@ -989,7 +990,8 @@ export class CustomModesManager { // Refresh the modes after import await this.refreshMergedState() - return { success: true } + // Return the imported mode's slug so the UI can activate it + return { success: true, slug: importData.customModes[0]?.slug } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logger.error("Failed to import mode with rules", { error: errorMessage }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index af5f9925c353..8c13df475834 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2222,10 +2222,11 @@ export const webviewMessageHandler = async ( await updateGlobalState("customModes", customModes) await provider.postStateToWebview() - // Send success message to webview + // Send success message to webview, include the imported slug so UI can switch provider.postMessageToWebview({ type: "importModeResult", success: true, + slug: result.slug, }) // Show success message diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index c50996585fe7..760935368f99 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -186,6 +186,17 @@ const ModesView = ({ onDone }: ModesViewProps) => { [visualMode, switchMode], ) + // Keep latest handleModeSwitch and customModes available inside window message handler + const handleModeSwitchRef = useRef(handleModeSwitch) + useEffect(() => { + handleModeSwitchRef.current = handleModeSwitch + }, [handleModeSwitch]) + + const customModesRef = useRef(customModes) + useEffect(() => { + customModesRef.current = customModes + }, [customModes]) + // Handler for popover open state change const onOpenChange = useCallback((open: boolean) => { setOpen(open) @@ -460,7 +471,21 @@ const ModesView = ({ onDone }: ModesViewProps) => { setIsImporting(false) setShowImportDialog(false) - if (!message.success) { + if (message.success) { + const slug = (message as any).slug as string | undefined + if (slug) { + // Try switching using the freshest mode list available + const all = getAllModes(customModesRef.current) + const importedMode = all.find((m) => m.slug === slug) + if (importedMode) { + handleModeSwitchRef.current(importedMode) + } else { + // Fallback: switch by slug to keep backend in sync and update visual selection + setVisualMode(slug) + switchMode(slug) + } + } + } else { // Only log error if it's not a cancellation if (message.error !== "cancelled") { console.error("Failed to import mode:", message.error) diff --git a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx new file mode 100644 index 000000000000..c31f8480ed94 --- /dev/null +++ b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx @@ -0,0 +1,94 @@ +import { render, screen, waitFor } from "@/utils/test-utils" +import ModesView from "../ModesView" +import { ExtensionStateContext } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +vitest.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vitest.fn(), + }, +})) + +const baseState = { + customModePrompts: {}, + listApiConfigMeta: [], + enhancementApiConfigId: "", + setEnhancementApiConfigId: vitest.fn(), + mode: "code", + customModes: [], + customSupportPrompts: [], + currentApiConfigName: "", + customInstructions: "", + setCustomInstructions: vitest.fn(), +} + +describe("ModesView - auto switch after import", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + it("switches to imported mode when import succeeds and slug is provided", async () => { + const importedMode = { + slug: "imported-mode", + name: "Imported Mode", + roleDefinition: "Role", + groups: ["read"] as const, + source: "global" as const, + } + + render( + + + , + ) + + const trigger = screen.getByTestId("mode-select-trigger") + expect(trigger).toHaveTextContent("Code") + + // Simulate extension sending successful import result with slug + window.dispatchEvent( + new MessageEvent("message", { + data: { type: "importModeResult", success: true, slug: "imported-mode" }, + }), + ) + + // Backend switch message sent + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", text: "imported-mode" }) + }) + + // UI reflects new mode selection + await waitFor(() => { + expect(trigger).toHaveTextContent("Imported Mode") + }) + }) + + it("does not switch when import fails or slug missing", async () => { + render( + + + , + ) + + const trigger = screen.getByTestId("mode-select-trigger") + expect(trigger).toHaveTextContent("Code") + + // Import failure + window.dispatchEvent( + new MessageEvent("message", { data: { type: "importModeResult", success: false, error: "x" } }), + ) + + await waitFor(() => { + expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) }) + }) + expect(trigger).toHaveTextContent("Code") + + // Success but no slug provided + window.dispatchEvent(new MessageEvent("message", { data: { type: "importModeResult", success: true } })) + + await waitFor(() => { + expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) }) + }) + expect(trigger).toHaveTextContent("Code") + }) +}) From 2bdaff028c030ccd933b0118d725009bc37f93fd Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 14:26:07 -0700 Subject: [PATCH 02/10] fix(modes): resolve react-hooks/exhaustive-deps in window message listener Use a stable ref to hold the latest switchMode and avoid capturing it in the window "message" listener closure: Add a switchModeRef updated via useEffect; call switchModeRef.current in the importModeResult fallback instead of switchMode Mirrors existing handleModeSwitchRef/customModesRef pattern to keep handler stable while accessing fresh values Prevents re-registration churn and removes the ESLint warning with no runtime behavior change File touched: webview-ui/src/components/modes/ModesView.tsx Why: The effect that registers the window event listener intentionally has an empty dependency array; directly referencing switchMode violates react-hooks/exhaustive-deps. Using a ref decouples the effect from function identity changes while preserving up-to-date behavior. --- webview-ui/src/components/modes/ModesView.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 760935368f99..d043bcff3776 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -197,6 +197,12 @@ const ModesView = ({ onDone }: ModesViewProps) => { customModesRef.current = customModes }, [customModes]) + // Keep latest switchMode available inside window message handler + const switchModeRef = useRef(switchMode) + useEffect(() => { + switchModeRef.current = switchMode + }, [switchMode]) + // Handler for popover open state change const onOpenChange = useCallback((open: boolean) => { setOpen(open) @@ -482,7 +488,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { } else { // Fallback: switch by slug to keep backend in sync and update visual selection setVisualMode(slug) - switchMode(slug) + switchModeRef.current?.(slug) } } } else { From 61d746e2a9115f0ef96b381a6ceda913f1ff406a Mon Sep 17 00:00:00 2001 From: Seth Miller Date: Sun, 5 Oct 2025 14:50:51 -0700 Subject: [PATCH 03/10] Update webview-ui/src/components/modes/ModesView.tsx Type safety for slug Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> --- webview-ui/src/components/modes/ModesView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index d043bcff3776..f333a33fc782 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -478,7 +478,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { setShowImportDialog(false) if (message.success) { - const slug = (message as any).slug as string | undefined +// Type safe access to slug +type ImportModeResult = { type: 'importModeResult'; success: boolean; slug?: string; error?: string } +const { slug } = message as ImportModeResult if (slug) { // Try switching using the freshest mode list available const all = getAllModes(customModesRef.current) From 90ab20bcf55d1b3f6023e769d41d1f0e1cea6994 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 15:29:10 -0700 Subject: [PATCH 04/10] fix: propagate updateCustomMode errors to prevent false-positive import success updateCustomMode() was catching and swallowing all errors, causing importModeWithRules() to return success even when mode persistence failed. This led to auto-switch attempting to activate modes that never persisted. --- src/core/config/CustomModesManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 3ce96a5fb9b6..a243a9236bec 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -412,7 +412,7 @@ export class CustomModesManager { const errorMessage = `Invalid mode configuration: ${errorMessages}` logger.error("Mode validation failed", { slug, errors: validationResult.error.errors }) vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) - return + throw new Error(errorMessage) } const isProjectMode = config.source === "project" @@ -458,6 +458,7 @@ export class CustomModesManager { const errorMessage = error instanceof Error ? error.message : String(error) logger.error("Failed to update custom mode", { slug, error: errorMessage }) vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage })) + throw error } } From 34134f9bff3e48aaadc0c78a0fee2b0d465f0a40 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 15:44:28 -0700 Subject: [PATCH 05/10] refactor(modes): hoist ImportModeResult type to module scope Moves type declaration out of event handler to prevent redeclaration on every message event. --- webview-ui/src/components/modes/ModesView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index f333a33fc782..b21d0f6392ba 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -55,6 +55,8 @@ const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) type ModeSource = "global" | "project" +type ImportModeResult = { type: 'importModeResult'; success: boolean; slug?: string; error?: string } + type ModesViewProps = { onDone: () => void } @@ -478,9 +480,7 @@ const ModesView = ({ onDone }: ModesViewProps) => { setShowImportDialog(false) if (message.success) { -// Type safe access to slug -type ImportModeResult = { type: 'importModeResult'; success: boolean; slug?: string; error?: string } -const { slug } = message as ImportModeResult + const { slug } = message as ImportModeResult if (slug) { // Try switching using the freshest mode list available const all = getAllModes(customModesRef.current) From 6fe074d17c2e9297b60625716360edc25f9f0108 Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 15:58:31 -0700 Subject: [PATCH 06/10] fix(modes): wrap updateCustomMode in try-catch to prevent unhandled rejections while allowing importModeWithRules to detect persistence failures --- src/core/webview/webviewMessageHandler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 8c13df475834..142520dbabfe 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1981,6 +1981,7 @@ export const webviewMessageHandler = async ( break case "updateCustomMode": if (message.modeConfig) { + try { // Check if this is a new mode or an update to an existing mode const existingModes = await provider.customModesManager.getCustomModes() const isNewMode = !existingModes.some((mode) => mode.slug === message.modeConfig?.slug) @@ -2016,6 +2017,10 @@ export const webviewMessageHandler = async ( } } } + } catch (error) { + // Error already shown to user by updateCustomMode + // Just prevent unhandled rejection and skip state updates + } } break case "deleteCustomMode": From 3e3d3249ff8e9ab01e7cd82d62f8999a9a8b6c9e Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 16:28:17 -0700 Subject: [PATCH 07/10] fix(modes): address parser consistency, UI desync, and test coverage issues - Use parseYamlSafely() in importModeWithRules for consistent YAML parsing across the codebase, ensuring BOM stripping and character cleaning - Sync visualMode with context.mode to prevent UI desync when modes are switched programmatically from outside the component - Add test coverage for fallback branch when imported mode slug is not yet present in customModes state Fixes parser inconsistency (P2), visualMode desync risk (P3), and missing test coverage (P3) identified in code review. --- src/core/config/CustomModesManager.ts | 2 +- webview-ui/src/components/modes/ModesView.tsx | 5 ++++ .../ModesView.import-switch.spec.tsx | 24 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index a243a9236bec..d65dbdadc229 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -932,7 +932,7 @@ export class CustomModesManager { // Parse the YAML content with proper type validation let importData: ImportData try { - const parsed = yaml.parse(yamlContent) + const parsed = this.parseYamlSafely(yamlContent, '') // Validate the structure if (!parsed?.customModes || !Array.isArray(parsed.customModes) || parsed.customModes.length === 0) { diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index b21d0f6392ba..39d9237c55fe 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -205,6 +205,11 @@ const ModesView = ({ onDone }: ModesViewProps) => { switchModeRef.current = switchMode }, [switchMode]) + // Sync visualMode with backend mode changes to prevent desync + useEffect(() => { + setVisualMode(mode) + }, [mode]) + // Handler for popover open state change const onOpenChange = useCallback((open: boolean) => { setOpen(open) diff --git a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx index c31f8480ed94..8be38e4cce69 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx @@ -91,4 +91,28 @@ describe("ModesView - auto switch after import", () => { }) expect(trigger).toHaveTextContent("Code") }) + + it("uses fallback branch when imported slug not yet present in customModes", async () => { + // Render with empty customModes - imported mode hasn't been added to state yet + render( + + + , + ) + + const trigger = screen.getByTestId("mode-select-trigger") + expect(trigger).toHaveTextContent("Code") + + // Simulate successful import for a slug not yet in customModes (timing race condition) + window.dispatchEvent( + new MessageEvent("message", { + data: { type: "importModeResult", success: true, slug: "not-yet-loaded-mode" }, + }), + ) + + // Fallback branch should send backend switch message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", text: "not-yet-loaded-mode" }) + }) + }) }) From 5183aec434bcd8a422b38f76e97bf8dddbdaf3ce Mon Sep 17 00:00:00 2001 From: heyseth Date: Sun, 5 Oct 2025 16:37:00 -0700 Subject: [PATCH 08/10] revert: replace parseYamlSafely with yaml.parse --- src/core/config/CustomModesManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index d65dbdadc229..a243a9236bec 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -932,7 +932,7 @@ export class CustomModesManager { // Parse the YAML content with proper type validation let importData: ImportData try { - const parsed = this.parseYamlSafely(yamlContent, '') + const parsed = yaml.parse(yamlContent) // Validate the structure if (!parsed?.customModes || !Array.isArray(parsed.customModes) || parsed.customModes.length === 0) { From f07b1c065346b9bed6939c90dd7e54ab596de087 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 3 Nov 2025 17:46:39 -0500 Subject: [PATCH 09/10] feat: auto-switch to imported mode with architect fallback Based on PR #8521 by @heyseth (Seth Miller) with the following enhancements: Original work by Seth Miller: - Auto-switch to imported mode after successful import - Backend returns imported mode slug in importModeWithRules() - UI switches to imported mode when found in customModes list - Comprehensive test coverage for auto-switch functionality Additional improvements: - Use architect mode as fallback when imported slug not yet present - Updated test to expect architect mode in fallback scenario - Prevents UI desync during state refresh race conditions Co-authored-by: Seth Miller --- webview-ui/src/components/modes/ModesView.tsx | 18 +- .../ModesView.import-switch.spec.tsx | 179 +++++++++++------- 2 files changed, 118 insertions(+), 79 deletions(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 39d9237c55fe..2b277c906d43 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -20,6 +20,7 @@ import { getCustomInstructions, getAllModes, findModeBySlug as findCustomModeBySlug, + defaultModeSlug, } from "@roo/modes" import { TOOL_GROUPS } from "@roo/tools" @@ -55,7 +56,7 @@ const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) type ModeSource = "global" | "project" -type ImportModeResult = { type: 'importModeResult'; success: boolean; slug?: string; error?: string } +type ImportModeResult = { type: "importModeResult"; success: boolean; slug?: string; error?: string } type ModesViewProps = { onDone: () => void @@ -188,19 +189,20 @@ const ModesView = ({ onDone }: ModesViewProps) => { [visualMode, switchMode], ) - // Keep latest handleModeSwitch and customModes available inside window message handler + // Refs to track latest state/functions for message handler (which has no dependencies) const handleModeSwitchRef = useRef(handleModeSwitch) + const customModesRef = useRef(customModes) + const switchModeRef = useRef(switchMode) + + // Update refs when dependencies change useEffect(() => { handleModeSwitchRef.current = handleModeSwitch }, [handleModeSwitch]) - const customModesRef = useRef(customModes) useEffect(() => { customModesRef.current = customModes }, [customModes]) - // Keep latest switchMode available inside window message handler - const switchModeRef = useRef(switchMode) useEffect(() => { switchModeRef.current = switchMode }, [switchMode]) @@ -493,9 +495,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { if (importedMode) { handleModeSwitchRef.current(importedMode) } else { - // Fallback: switch by slug to keep backend in sync and update visual selection - setVisualMode(slug) - switchModeRef.current?.(slug) + // Fallback: slug not yet in state (race condition) - select architect mode + setVisualMode("architect") + switchModeRef.current?.("architect") } } } else { diff --git a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx index 8be38e4cce69..fe05c17dc4cd 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx @@ -1,17 +1,23 @@ -import { render, screen, waitFor } from "@/utils/test-utils" +// npx vitest src/components/modes/__tests__/ModesView.import-switch.spec.tsx + +import { render, waitFor } from "@/utils/test-utils" import ModesView from "../ModesView" import { ExtensionStateContext } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" +// Mock vscode API vitest.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vitest.fn(), }, })) -const baseState = { +const mockExtensionState = { customModePrompts: {}, - listApiConfigMeta: [], + listApiConfigMeta: [ + { id: "config1", name: "Config 1" }, + { id: "config2", name: "Config 2" }, + ], enhancementApiConfigId: "", setEnhancementApiConfigId: vitest.fn(), mode: "code", @@ -22,97 +28,128 @@ const baseState = { setCustomInstructions: vitest.fn(), } -describe("ModesView - auto switch after import", () => { +const renderModesView = (props = {}) => { + const mockOnDone = vitest.fn() + return render( + + + , + ) +} + +Element.prototype.scrollIntoView = vitest.fn() + +describe("ModesView Import Auto-Switch", () => { beforeEach(() => { vitest.clearAllMocks() }) - it("switches to imported mode when import succeeds and slug is provided", async () => { - const importedMode = { - slug: "imported-mode", - name: "Imported Mode", - roleDefinition: "Role", - groups: ["read"] as const, - source: "global" as const, + it("should auto-switch to imported mode when found in current state", async () => { + const importedModeSlug = "custom-test-mode" + const customModes = [ + { + slug: importedModeSlug, + name: "Custom Test Mode", + roleDefinition: "Test role", + groups: [], + }, + ] + + renderModesView({ customModes }) + + // Simulate successful import message with the mode already in state + const importMessage = { + data: { + type: "importModeResult", + success: true, + slug: importedModeSlug, + }, } - render( - - - , - ) - - const trigger = screen.getByTestId("mode-select-trigger") - expect(trigger).toHaveTextContent("Code") - - // Simulate extension sending successful import result with slug - window.dispatchEvent( - new MessageEvent("message", { - data: { type: "importModeResult", success: true, slug: "imported-mode" }, - }), - ) - - // Backend switch message sent - await waitFor(() => { - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", text: "imported-mode" }) - }) + window.dispatchEvent(new MessageEvent("message", importMessage)) - // UI reflects new mode selection + // Wait for the mode switch message to be sent await waitFor(() => { - expect(trigger).toHaveTextContent("Imported Mode") + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "mode", + text: importedModeSlug, + }) }) }) - it("does not switch when import fails or slug missing", async () => { - render( - - - , - ) - - const trigger = screen.getByTestId("mode-select-trigger") - expect(trigger).toHaveTextContent("Code") + it("should fallback to architect mode when imported slug not yet in state (race condition)", async () => { + const importedModeSlug = "custom-new-mode" - // Import failure - window.dispatchEvent( - new MessageEvent("message", { data: { type: "importModeResult", success: false, error: "x" } }), - ) + // Render without the imported mode in customModes (simulating race condition) + renderModesView({ customModes: [] }) - await waitFor(() => { - expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) }) - }) - expect(trigger).toHaveTextContent("Code") + // Simulate successful import message but mode not yet in state + const importMessage = { + data: { + type: "importModeResult", + success: true, + slug: importedModeSlug, + }, + } - // Success but no slug provided - window.dispatchEvent(new MessageEvent("message", { data: { type: "importModeResult", success: true } })) + window.dispatchEvent(new MessageEvent("message", importMessage)) + // Wait for the fallback to architect mode await waitFor(() => { - expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) }) + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "mode", + text: "architect", + }) }) - expect(trigger).toHaveTextContent("Code") }) - it("uses fallback branch when imported slug not yet present in customModes", async () => { - // Render with empty customModes - imported mode hasn't been added to state yet - render( - - - , - ) + it("should not switch modes on import failure", async () => { + renderModesView() + + // Simulate failed import message + const importMessage = { + data: { + type: "importModeResult", + success: false, + error: "Import failed", + }, + } + + window.dispatchEvent(new MessageEvent("message", importMessage)) - const trigger = screen.getByTestId("mode-select-trigger") - expect(trigger).toHaveTextContent("Code") + // Wait a bit to ensure no mode switch happens + await new Promise((resolve) => setTimeout(resolve, 100)) - // Simulate successful import for a slug not yet in customModes (timing race condition) - window.dispatchEvent( - new MessageEvent("message", { - data: { type: "importModeResult", success: true, slug: "not-yet-loaded-mode" }, + // Verify no mode switch message was sent + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "mode", }), ) + }) - // Fallback branch should send backend switch message - await waitFor(() => { - expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", text: "not-yet-loaded-mode" }) - }) + it("should not switch modes on cancelled import", async () => { + renderModesView() + + // Simulate cancelled import message + const importMessage = { + data: { + type: "importModeResult", + success: false, + error: "cancelled", + }, + } + + window.dispatchEvent(new MessageEvent("message", importMessage)) + + // Wait a bit to ensure no mode switch happens + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Verify no mode switch message was sent + expect(vscode.postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "mode", + }), + ) }) }) From 387acca27ebd4b60650fe1d3960a3731859ac91c Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 3 Nov 2025 17:54:12 -0500 Subject: [PATCH 10/10] fix: use defaultModeSlug instead of hardcoded architect for better maintainability --- webview-ui/src/components/modes/ModesView.tsx | 6 +++--- .../modes/__tests__/ModesView.import-switch.spec.tsx | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 2b277c906d43..1107eb44e10a 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -495,9 +495,9 @@ const ModesView = ({ onDone }: ModesViewProps) => { if (importedMode) { handleModeSwitchRef.current(importedMode) } else { - // Fallback: slug not yet in state (race condition) - select architect mode - setVisualMode("architect") - switchModeRef.current?.("architect") + // Fallback: slug not yet in state (race condition) - select default mode + setVisualMode(defaultModeSlug) + switchModeRef.current?.(defaultModeSlug) } } } else { diff --git a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx index fe05c17dc4cd..b62fc8856368 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.import-switch.spec.tsx @@ -4,6 +4,7 @@ import { render, waitFor } from "@/utils/test-utils" import ModesView from "../ModesView" import { ExtensionStateContext } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" +import { defaultModeSlug } from "@roo/modes" // Mock vscode API vitest.mock("@src/utils/vscode", () => ({ @@ -94,11 +95,11 @@ describe("ModesView Import Auto-Switch", () => { window.dispatchEvent(new MessageEvent("message", importMessage)) - // Wait for the fallback to architect mode + // Wait for the fallback to default mode (architect) await waitFor(() => { expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", - text: "architect", + text: defaultModeSlug, }) }) })