Skip to content

Commit a210924

Browse files
committed
Merge branch 'main' into cte/deep-research
2 parents 0649ac9 + 3917371 commit a210924

File tree

9 files changed

+197
-13
lines changed

9 files changed

+197
-13
lines changed

.changeset/twelve-ways-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Create .roomodes file if needed

.vscodeignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ demo.gif
2121
.nvmrc
2222
.gitattributes
2323
.prettierignore
24+
.clinerules*
25+
.roomodes
26+
cline_docs/**
27+
coverage/**
28+
out-integration/**
2429

2530
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
2631
webview-ui/src/**
@@ -41,4 +46,4 @@ webview-ui/node_modules/**
4146
!src/integrations/theme/default-themes/**
4247

4348
# Include icons
44-
!assets/icons/**
49+
!assets/icons/**

src/core/config/CustomModesManager.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CustomModesSettingsSchema } from "./CustomModesSchema"
55
import { ModeConfig } from "../../shared/modes"
66
import { fileExistsAtPath } from "../../utils/fs"
77
import { arePathsEqual } from "../../utils/path"
8+
import { logger } from "../../utils/logging"
89

910
const ROOMODES_FILENAME = ".roomodes"
1011

@@ -214,14 +215,26 @@ export class CustomModesManager {
214215
await this.context.globalState.update("customModes", mergedModes)
215216
return mergedModes
216217
}
217-
218218
async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
219219
try {
220220
const isProjectMode = config.source === "project"
221-
const targetPath = isProjectMode ? await this.getWorkspaceRoomodes() : await this.getCustomModesFilePath()
221+
let targetPath: string
222222

223-
if (isProjectMode && !targetPath) {
224-
throw new Error("No workspace folder found for project-specific mode")
223+
if (isProjectMode) {
224+
const workspaceFolders = vscode.workspace.workspaceFolders
225+
if (!workspaceFolders || workspaceFolders.length === 0) {
226+
logger.error("Failed to update project mode: No workspace folder found", { slug })
227+
throw new Error("No workspace folder found for project-specific mode")
228+
}
229+
const workspaceRoot = workspaceFolders[0].uri.fsPath
230+
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
231+
const exists = await fileExistsAtPath(targetPath)
232+
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
233+
slug,
234+
workspace: workspaceRoot,
235+
})
236+
} else {
237+
targetPath = await this.getCustomModesFilePath()
225238
}
226239

227240
await this.queueWrite(async () => {
@@ -231,7 +244,7 @@ export class CustomModesManager {
231244
source: isProjectMode ? ("project" as const) : ("global" as const),
232245
}
233246

234-
await this.updateModesInFile(targetPath!, (modes) => {
247+
await this.updateModesInFile(targetPath, (modes) => {
235248
const updatedModes = modes.filter((m) => m.slug !== slug)
236249
updatedModes.push(modeWithSource)
237250
return updatedModes
@@ -240,9 +253,9 @@ export class CustomModesManager {
240253
await this.refreshMergedState()
241254
})
242255
} catch (error) {
243-
vscode.window.showErrorMessage(
244-
`Failed to update custom mode: ${error instanceof Error ? error.message : String(error)}`,
245-
)
256+
const errorMessage = error instanceof Error ? error.message : String(error)
257+
logger.error("Failed to update custom mode", { slug, error: errorMessage })
258+
vscode.window.showErrorMessage(`Failed to update custom mode: ${errorMessage}`)
246259
}
247260
}
248261
private async updateModesInFile(filePath: string, operation: (modes: ModeConfig[]) => ModeConfig[]): Promise<void> {

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,57 @@ describe("CustomModesManager", () => {
207207
expect(mockOnUpdate).toHaveBeenCalled()
208208
})
209209

210+
it("creates .roomodes file when adding project-specific mode", async () => {
211+
const projectMode: ModeConfig = {
212+
slug: "project-mode",
213+
name: "Project Mode",
214+
roleDefinition: "Project Role",
215+
groups: ["read"],
216+
source: "project",
217+
}
218+
219+
// Mock .roomodes to not exist initially
220+
let roomodesContent: any = null
221+
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
222+
return path === mockSettingsPath
223+
})
224+
;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => {
225+
if (path === mockSettingsPath) {
226+
return JSON.stringify({ customModes: [] })
227+
}
228+
if (path === mockRoomodes) {
229+
if (!roomodesContent) {
230+
throw new Error("File not found")
231+
}
232+
return JSON.stringify(roomodesContent)
233+
}
234+
throw new Error("File not found")
235+
})
236+
;(fs.writeFile as jest.Mock).mockImplementation(async (path: string, content: string) => {
237+
if (path === mockRoomodes) {
238+
roomodesContent = JSON.parse(content)
239+
}
240+
return Promise.resolve()
241+
})
242+
243+
await manager.updateCustomMode("project-mode", projectMode)
244+
245+
// Verify .roomodes was created with the project mode
246+
expect(fs.writeFile).toHaveBeenCalledWith(mockRoomodes, expect.stringContaining("project-mode"), "utf-8")
247+
248+
// Verify the content written to .roomodes
249+
expect(roomodesContent).toEqual({
250+
customModes: [
251+
expect.objectContaining({
252+
slug: "project-mode",
253+
name: "Project Mode",
254+
roleDefinition: "Project Role",
255+
source: "project",
256+
}),
257+
],
258+
})
259+
})
260+
210261
it("queues write operations", async () => {
211262
const mode1: ModeConfig = {
212263
slug: "mode1",

src/core/webview/ClineProvider.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
951951
}
952952
break
953953
}
954+
case "deleteMcpServer": {
955+
if (!message.serverName) {
956+
break
957+
}
958+
959+
try {
960+
this.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`)
961+
await this.mcpHub?.deleteServer(message.serverName)
962+
this.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`)
963+
} catch (error) {
964+
const errorMessage = error instanceof Error ? error.message : String(error)
965+
this.outputChannel.appendLine(`Failed to delete MCP server: ${errorMessage}`)
966+
// Error messages are already handled by McpHub.deleteServer
967+
}
968+
break
969+
}
954970
case "restartMcpServer": {
955971
try {
956972
await this.mcpHub?.restartConnection(message.text!)

src/services/mcp/McpHub.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,59 @@ export class McpHub {
576576
}
577577
}
578578

579+
public async deleteServer(serverName: string): Promise<void> {
580+
try {
581+
const settingsPath = await this.getMcpSettingsFilePath()
582+
583+
// Ensure the settings file exists and is accessible
584+
try {
585+
await fs.access(settingsPath)
586+
} catch (error) {
587+
throw new Error("Settings file not accessible")
588+
}
589+
590+
const content = await fs.readFile(settingsPath, "utf-8")
591+
const config = JSON.parse(content)
592+
593+
// Validate the config structure
594+
if (!config || typeof config !== "object") {
595+
throw new Error("Invalid config structure")
596+
}
597+
598+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
599+
config.mcpServers = {}
600+
}
601+
602+
// Remove the server from the settings
603+
if (config.mcpServers[serverName]) {
604+
delete config.mcpServers[serverName]
605+
606+
// Write the entire config back
607+
const updatedConfig = {
608+
mcpServers: config.mcpServers,
609+
}
610+
611+
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
612+
613+
// Update server connections
614+
await this.updateServerConnections(config.mcpServers)
615+
616+
vscode.window.showInformationMessage(`Deleted MCP server: ${serverName}`)
617+
} else {
618+
vscode.window.showWarningMessage(`Server "${serverName}" not found in configuration`)
619+
}
620+
} catch (error) {
621+
console.error("Failed to delete MCP server:", error)
622+
if (error instanceof Error) {
623+
console.error("Error details:", error.message, error.stack)
624+
}
625+
vscode.window.showErrorMessage(
626+
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
627+
)
628+
throw error
629+
}
630+
}
631+
579632
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
580633
const connection = this.connections.find((conn) => conn.server.name === serverName)
581634
if (!connection) {

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export interface WebviewMessage {
9393
| "openCustomModesSettings"
9494
| "checkpointDiff"
9595
| "checkpointRestore"
96+
| "deleteMcpServer"
9697
| "maxOpenTabsContext"
9798
| "research.task"
9899
| "research.input"

webview-ui/src/components/mcp/McpView.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { McpServer } from "../../../../src/shared/mcp"
1313
import McpToolRow from "./McpToolRow"
1414
import McpResourceRow from "./McpResourceRow"
1515
import McpEnabledToggle from "./McpEnabledToggle"
16+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
1617

1718
type McpViewProps = {
1819
onDone: () => void
@@ -129,6 +130,7 @@ const McpView = ({ onDone }: McpViewProps) => {
129130
// Server Row Component
130131
const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
131132
const [isExpanded, setIsExpanded] = useState(false)
133+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
132134
const [timeoutValue, setTimeoutValue] = useState(() => {
133135
const configTimeout = JSON.parse(server.config)?.timeout
134136
return configTimeout ?? 60 // Default 1 minute (60 seconds)
@@ -179,6 +181,14 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
179181
})
180182
}
181183

184+
const handleDelete = () => {
185+
vscode.postMessage({
186+
type: "deleteMcpServer",
187+
serverName: server.name,
188+
})
189+
setShowDeleteConfirm(false)
190+
}
191+
182192
return (
183193
<div style={{ marginBottom: "10px" }}>
184194
<div
@@ -202,6 +212,12 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
202212
<div
203213
style={{ display: "flex", alignItems: "center", marginRight: "8px" }}
204214
onClick={(e) => e.stopPropagation()}>
215+
<VSCodeButton
216+
appearance="icon"
217+
onClick={() => setShowDeleteConfirm(true)}
218+
style={{ marginRight: "8px" }}>
219+
<span className="codicon codicon-trash" style={{ fontSize: "14px" }}></span>
220+
</VSCodeButton>
205221
<VSCodeButton
206222
appearance="icon"
207223
onClick={handleRestart}
@@ -393,6 +409,27 @@ const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowM
393409
</div>
394410
)
395411
)}
412+
413+
{/* Delete Confirmation Dialog */}
414+
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
415+
<DialogContent>
416+
<DialogHeader>
417+
<DialogTitle>Delete MCP Server</DialogTitle>
418+
<DialogDescription>
419+
Are you sure you want to delete the MCP server "{server.name}"? This action cannot be
420+
undone.
421+
</DialogDescription>
422+
</DialogHeader>
423+
<DialogFooter>
424+
<VSCodeButton appearance="secondary" onClick={() => setShowDeleteConfirm(false)}>
425+
Cancel
426+
</VSCodeButton>
427+
<VSCodeButton appearance="primary" onClick={handleDelete}>
428+
Delete
429+
</VSCodeButton>
430+
</DialogFooter>
431+
</DialogContent>
432+
</Dialog>
396433
</div>
397434
)
398435
}

webview-ui/src/components/prompts/PromptsView.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -892,10 +892,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
892892
appearance="icon"
893893
title="Copy system prompt to clipboard"
894894
onClick={() => {
895-
vscode.postMessage({
896-
type: "copySystemPrompt",
897-
text: selectedPromptContent,
898-
})
895+
const currentMode = getCurrentMode()
896+
if (currentMode) {
897+
vscode.postMessage({
898+
type: "copySystemPrompt",
899+
mode: currentMode.slug,
900+
})
901+
}
899902
}}
900903
data-testid="copy-prompt-button">
901904
<span className="codicon codicon-copy"></span>

0 commit comments

Comments
 (0)