Skip to content

Commit 6d11f44

Browse files
committed
Find the current mode in the list before switching
1 parent e5a8ca9 commit 6d11f44

File tree

2 files changed

+252
-13
lines changed

2 files changed

+252
-13
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import * as vscode from "vscode"
2+
import { registerModeSwitchingCommands } from "../mode-switching"
3+
import { ClineProvider } from "../../core/webview/ClineProvider"
4+
import { ModeConfig, modes, getAllModes } from "../../shared/modes"
5+
import { logger } from "../../utils/logging"
6+
7+
// Mock dependencies
8+
jest.mock("../../core/webview/ClineProvider", () => ({
9+
ClineProvider: {
10+
getInstance: jest.fn(),
11+
},
12+
}))
13+
14+
jest.mock("../../utils/logging", () => ({
15+
logger: {
16+
info: jest.fn(),
17+
error: jest.fn(),
18+
},
19+
}))
20+
21+
jest.mock("../../shared/modes", () => ({
22+
modes: [
23+
{ slug: "code", name: "Code", roleDefinition: "Code role" },
24+
{ slug: "architect", name: "Architect", roleDefinition: "Architect role" },
25+
],
26+
getAllModes: jest.fn(),
27+
}))
28+
29+
// Simple mock for vscode
30+
jest.mock("vscode", () => ({
31+
commands: {
32+
registerCommand: jest.fn(),
33+
},
34+
window: {
35+
showErrorMessage: jest.fn(),
36+
},
37+
}))
38+
39+
describe("registerModeSwitchingCommands", () => {
40+
// Mock context
41+
const mockContext = {
42+
subscriptions: [],
43+
// Add more required properties
44+
workspaceState: { get: jest.fn(), update: jest.fn() },
45+
globalState: { get: jest.fn(), update: jest.fn(), keys: jest.fn() },
46+
secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
47+
extensionUri: {} as vscode.Uri,
48+
extensionPath: "/test/path",
49+
} as unknown as vscode.ExtensionContext
50+
51+
// Get reference to the mocked registerCommand
52+
const mockRegisterCommand = vscode.commands.registerCommand as jest.Mock
53+
54+
beforeEach(() => {
55+
// Clear all mocks before each test
56+
jest.clearAllMocks()
57+
58+
// Default mock implementations
59+
mockRegisterCommand.mockImplementation((commandId, callback) => {
60+
return { dispose: jest.fn() }
61+
})
62+
})
63+
64+
it("should register the cycleModes command", () => {
65+
// Act
66+
registerModeSwitchingCommands(mockContext)
67+
68+
// Assert
69+
expect(mockRegisterCommand).toHaveBeenCalledWith("roo-cline.cycleModes", expect.any(Function))
70+
expect(mockContext.subscriptions.length).toBe(1)
71+
})
72+
73+
describe("cycleModes command", () => {
74+
// Setup to capture the command callback
75+
let cycleModeCallback: Function
76+
77+
beforeEach(() => {
78+
mockRegisterCommand.mockImplementation((commandId, callback) => {
79+
if (commandId === "roo-cline.cycleModes") {
80+
cycleModeCallback = callback
81+
}
82+
return { dispose: jest.fn() }
83+
})
84+
85+
// Register commands to capture the callback
86+
registerModeSwitchingCommands(mockContext)
87+
})
88+
89+
it("should cycle to the next mode when current mode is found", async () => {
90+
// Arrange
91+
const allModes = [
92+
...modes,
93+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
94+
]
95+
;(getAllModes as jest.Mock).mockReturnValue(allModes)
96+
97+
const mockProvider = {
98+
getState: jest.fn().mockResolvedValue({
99+
mode: "code",
100+
customModes: [
101+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
102+
],
103+
}),
104+
postMessageToWebview: jest.fn().mockResolvedValue(undefined),
105+
}
106+
;(ClineProvider.getInstance as jest.Mock).mockResolvedValue(mockProvider)
107+
108+
// Find the index of 'code' mode in all modes
109+
const codeIndex = allModes.findIndex((mode) => mode.slug === "code")
110+
const expectedNextMode = allModes[(codeIndex + 1) % allModes.length]
111+
112+
// Act
113+
await cycleModeCallback()
114+
115+
// Assert
116+
expect(mockProvider.getState).toHaveBeenCalled()
117+
expect(getAllModes).toHaveBeenCalledWith([
118+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
119+
])
120+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
121+
type: "mode",
122+
text: expectedNextMode.slug,
123+
})
124+
})
125+
126+
it("should default to first mode when current mode is not found", async () => {
127+
// Arrange
128+
const customModes = [
129+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
130+
]
131+
const allModes = [...modes, ...customModes]
132+
;(getAllModes as jest.Mock).mockReturnValue(allModes)
133+
134+
const mockProvider = {
135+
getState: jest.fn().mockResolvedValue({
136+
mode: "nonexistent-mode",
137+
customModes: customModes,
138+
}),
139+
postMessageToWebview: jest.fn().mockResolvedValue(undefined),
140+
}
141+
;(ClineProvider.getInstance as jest.Mock).mockResolvedValue(mockProvider)
142+
143+
// First mode in the list of all modes
144+
const expectedNextMode = allModes[0]
145+
146+
// Act
147+
await cycleModeCallback()
148+
149+
// Assert
150+
expect(mockProvider.getState).toHaveBeenCalled()
151+
expect(getAllModes).toHaveBeenCalledWith(customModes)
152+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
153+
type: "mode",
154+
text: expectedNextMode.slug,
155+
})
156+
})
157+
158+
it("should handle errors when provider is not available", async () => {
159+
// Arrange
160+
;(ClineProvider.getInstance as jest.Mock).mockResolvedValue(null)
161+
162+
// Act
163+
await cycleModeCallback()
164+
165+
// Assert
166+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to switch mode"))
167+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
168+
expect.stringContaining("Failed to switch mode"),
169+
)
170+
})
171+
172+
it("should handle errors in getState", async () => {
173+
// Arrange
174+
const allModes = [
175+
...modes,
176+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
177+
]
178+
;(getAllModes as jest.Mock).mockReturnValue(allModes)
179+
180+
const mockProvider = {
181+
getState: jest.fn().mockRejectedValue(new Error("State error")),
182+
postMessageToWebview: jest.fn().mockResolvedValue(undefined),
183+
}
184+
;(ClineProvider.getInstance as jest.Mock).mockResolvedValue(mockProvider)
185+
186+
// Act
187+
await cycleModeCallback()
188+
189+
// Assert
190+
expect(mockProvider.getState).toHaveBeenCalled()
191+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
192+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to switch mode"))
193+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
194+
expect.stringContaining("Failed to switch mode"),
195+
)
196+
})
197+
198+
it("should handle errors in postMessageToWebview", async () => {
199+
// Arrange
200+
const customModes = [
201+
{ slug: "custom-mode", name: "Custom Mode", roleDefinition: "Custom role", groups: [] },
202+
]
203+
const allModes = [...modes, ...customModes]
204+
;(getAllModes as jest.Mock).mockReturnValue(allModes)
205+
206+
const mockProvider = {
207+
getState: jest.fn().mockResolvedValue({ mode: "code", customModes }),
208+
postMessageToWebview: jest.fn().mockRejectedValue(new Error("Webview error")),
209+
}
210+
;(ClineProvider.getInstance as jest.Mock).mockResolvedValue(mockProvider)
211+
212+
// Act
213+
await cycleModeCallback()
214+
215+
// Assert
216+
expect(mockProvider.getState).toHaveBeenCalled()
217+
expect(getAllModes).toHaveBeenCalledWith(customModes)
218+
expect(mockProvider.postMessageToWebview).toHaveBeenCalled()
219+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("Failed to switch mode"))
220+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
221+
expect.stringContaining("Failed to switch mode"),
222+
)
223+
})
224+
})
225+
})

src/commands/mode-switching.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
import * as vscode from "vscode"
2-
import { modes } from "../shared/modes"
2+
import { ModeConfig, modes, getAllModes } from "../shared/modes"
33
import { ClineProvider } from "../core/webview/ClineProvider"
4+
import { logger } from "../utils/logging"
45

56
export function registerModeSwitchingCommands(context: vscode.ExtensionContext) {
6-
let currentModeIndex = 0
7-
87
context.subscriptions.push(
98
vscode.commands.registerCommand("roo-cline.cycleModes", async () => {
109
try {
11-
// Get next mode index
12-
currentModeIndex = (currentModeIndex + 1) % modes.length
13-
const nextMode = modes[currentModeIndex]
14-
15-
// Get visible instance and send message
10+
// Get provider instance
1611
const provider = await ClineProvider.getInstance()
17-
if (provider) {
18-
await provider.postMessageToWebview({
19-
type: "mode",
20-
text: nextMode.slug,
21-
})
12+
if (!provider) {
13+
throw new Error("No active Cline provider found")
2214
}
15+
16+
// Get current mode and custom modes from provider state
17+
const state = await provider.getState()
18+
const currentModeSlug = state.mode
19+
const customModes = state.customModes || []
20+
21+
// Get all modes including custom modes
22+
const allModes = getAllModes(customModes)
23+
24+
// Find current mode index in the combined array
25+
const currentModeIndex = allModes.findIndex((mode) => mode.slug === currentModeSlug)
26+
27+
// Get next mode index, defaulting to first mode if current mode not found
28+
const nextModeIndex = (currentModeIndex + 1) % allModes.length
29+
const nextMode = allModes[nextModeIndex]
30+
31+
// Send message to webview to switch mode
32+
await provider.postMessageToWebview({
33+
type: "mode",
34+
text: nextMode.slug,
35+
})
2336
} catch (error) {
37+
logger.error(`Failed to switch mode: ${error}`)
2438
vscode.window.showErrorMessage(`Failed to switch mode: ${error}`)
2539
}
2640
}),

0 commit comments

Comments
 (0)