Skip to content

Commit a2792d1

Browse files
committed
feat: auto-switch to imported mode after successful import
Fixes [#8239](#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
1 parent 85b0e8a commit a2792d1

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

src/core/config/CustomModesManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface ExportResult {
4040

4141
interface ImportResult {
4242
success: boolean
43+
slug?: string
4344
error?: string
4445
}
4546

@@ -989,7 +990,8 @@ export class CustomModesManager {
989990
// Refresh the modes after import
990991
await this.refreshMergedState()
991992

992-
return { success: true }
993+
// Return the imported mode's slug so the UI can activate it
994+
return { success: true, slug: importData.customModes[0]?.slug }
993995
} catch (error) {
994996
const errorMessage = error instanceof Error ? error.message : String(error)
995997
logger.error("Failed to import mode with rules", { error: errorMessage })

src/core/webview/webviewMessageHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2222,10 +2222,11 @@ export const webviewMessageHandler = async (
22222222
await updateGlobalState("customModes", customModes)
22232223
await provider.postStateToWebview()
22242224

2225-
// Send success message to webview
2225+
// Send success message to webview, include the imported slug so UI can switch
22262226
provider.postMessageToWebview({
22272227
type: "importModeResult",
22282228
success: true,
2229+
slug: result.slug,
22292230
})
22302231

22312232
// Show success message

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ const ModesView = ({ onDone }: ModesViewProps) => {
186186
[visualMode, switchMode],
187187
)
188188

189+
// Keep latest handleModeSwitch and customModes available inside window message handler
190+
const handleModeSwitchRef = useRef(handleModeSwitch)
191+
useEffect(() => {
192+
handleModeSwitchRef.current = handleModeSwitch
193+
}, [handleModeSwitch])
194+
195+
const customModesRef = useRef(customModes)
196+
useEffect(() => {
197+
customModesRef.current = customModes
198+
}, [customModes])
199+
189200
// Handler for popover open state change
190201
const onOpenChange = useCallback((open: boolean) => {
191202
setOpen(open)
@@ -460,7 +471,21 @@ const ModesView = ({ onDone }: ModesViewProps) => {
460471
setIsImporting(false)
461472
setShowImportDialog(false)
462473

463-
if (!message.success) {
474+
if (message.success) {
475+
const slug = (message as any).slug as string | undefined
476+
if (slug) {
477+
// Try switching using the freshest mode list available
478+
const all = getAllModes(customModesRef.current)
479+
const importedMode = all.find((m) => m.slug === slug)
480+
if (importedMode) {
481+
handleModeSwitchRef.current(importedMode)
482+
} else {
483+
// Fallback: switch by slug to keep backend in sync and update visual selection
484+
setVisualMode(slug)
485+
switchMode(slug)
486+
}
487+
}
488+
} else {
464489
// Only log error if it's not a cancellation
465490
if (message.error !== "cancelled") {
466491
console.error("Failed to import mode:", message.error)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { render, screen, waitFor } from "@/utils/test-utils"
2+
import ModesView from "../ModesView"
3+
import { ExtensionStateContext } from "@src/context/ExtensionStateContext"
4+
import { vscode } from "@src/utils/vscode"
5+
6+
vitest.mock("@src/utils/vscode", () => ({
7+
vscode: {
8+
postMessage: vitest.fn(),
9+
},
10+
}))
11+
12+
const baseState = {
13+
customModePrompts: {},
14+
listApiConfigMeta: [],
15+
enhancementApiConfigId: "",
16+
setEnhancementApiConfigId: vitest.fn(),
17+
mode: "code",
18+
customModes: [],
19+
customSupportPrompts: [],
20+
currentApiConfigName: "",
21+
customInstructions: "",
22+
setCustomInstructions: vitest.fn(),
23+
}
24+
25+
describe("ModesView - auto switch after import", () => {
26+
beforeEach(() => {
27+
vitest.clearAllMocks()
28+
})
29+
30+
it("switches to imported mode when import succeeds and slug is provided", async () => {
31+
const importedMode = {
32+
slug: "imported-mode",
33+
name: "Imported Mode",
34+
roleDefinition: "Role",
35+
groups: ["read"] as const,
36+
source: "global" as const,
37+
}
38+
39+
render(
40+
<ExtensionStateContext.Provider value={{ ...baseState, customModes: [importedMode] } as any}>
41+
<ModesView onDone={vitest.fn()} />
42+
</ExtensionStateContext.Provider>,
43+
)
44+
45+
const trigger = screen.getByTestId("mode-select-trigger")
46+
expect(trigger).toHaveTextContent("Code")
47+
48+
// Simulate extension sending successful import result with slug
49+
window.dispatchEvent(
50+
new MessageEvent("message", {
51+
data: { type: "importModeResult", success: true, slug: "imported-mode" },
52+
}),
53+
)
54+
55+
// Backend switch message sent
56+
await waitFor(() => {
57+
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "mode", text: "imported-mode" })
58+
})
59+
60+
// UI reflects new mode selection
61+
await waitFor(() => {
62+
expect(trigger).toHaveTextContent("Imported Mode")
63+
})
64+
})
65+
66+
it("does not switch when import fails or slug missing", async () => {
67+
render(
68+
<ExtensionStateContext.Provider value={{ ...baseState } as any}>
69+
<ModesView onDone={vitest.fn()} />
70+
</ExtensionStateContext.Provider>,
71+
)
72+
73+
const trigger = screen.getByTestId("mode-select-trigger")
74+
expect(trigger).toHaveTextContent("Code")
75+
76+
// Import failure
77+
window.dispatchEvent(
78+
new MessageEvent("message", { data: { type: "importModeResult", success: false, error: "x" } }),
79+
)
80+
81+
await waitFor(() => {
82+
expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) })
83+
})
84+
expect(trigger).toHaveTextContent("Code")
85+
86+
// Success but no slug provided
87+
window.dispatchEvent(new MessageEvent("message", { data: { type: "importModeResult", success: true } }))
88+
89+
await waitFor(() => {
90+
expect(vscode.postMessage).not.toHaveBeenCalledWith({ type: "mode", text: expect.any(String) })
91+
})
92+
expect(trigger).toHaveTextContent("Code")
93+
})
94+
})

0 commit comments

Comments
 (0)