Skip to content

Commit 450583c

Browse files
à la mode (RooCodeInc#2912)
* add fetching global cline rules files * add toggle functionality to clinerules * add toggles modal * changeset * change codicon * fix bad merged files * fix duplicate globalClineRulesToggles declaration in state.ts from merge * refresh cline rules on modal open
1 parent 45b1666 commit 450583c

File tree

14 files changed

+324
-29
lines changed

14 files changed

+324
-29
lines changed

.changeset/twelve-meals-applaud.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add modal UI for toggling Cline Rules

src/core/context/instructions/user-instructions/cline-rules.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import path from "path"
2-
import { GlobalFileNames } from "../../../storage/disk"
2+
import { ensureRulesDirectoryExists, GlobalFileNames } from "../../../storage/disk"
33
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
44
import { formatResponse } from "../../../prompts/responses"
55
import fs from "fs/promises"
6-
7-
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
6+
import { ClineRulesToggles } from "../../../../shared/cline-rules"
7+
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../storage/state"
8+
import * as vscode from "vscode"
89

910
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
1011
if (await fileExistsAtPath(globalClineRulesFilePath)) {
@@ -147,3 +148,28 @@ export async function synchronizeRuleToggles(
147148

148149
return updatedToggles
149150
}
151+
152+
export async function refreshClineRulesToggles(
153+
context: vscode.ExtensionContext,
154+
workingDirectory: string,
155+
): Promise<{
156+
globalToggles: ClineRulesToggles
157+
localToggles: ClineRulesToggles
158+
}> {
159+
// Global toggles
160+
const globalClineRulesToggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
161+
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
162+
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
163+
await updateGlobalState(context, "globalClineRulesToggles", updatedGlobalToggles)
164+
165+
// Local toggles
166+
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
167+
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
168+
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
169+
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
170+
171+
return {
172+
globalToggles: updatedGlobalToggles,
173+
localToggles: updatedLocalToggles,
174+
}
175+
}

src/core/controller/index.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@ import {
4040
getAllExtensionState,
4141
getGlobalState,
4242
getSecret,
43+
getWorkspaceState,
4344
resetExtensionState,
4445
storeSecret,
4546
updateApiConfiguration,
4647
updateGlobalState,
48+
updateWorkspaceState,
4749
} from "../storage/state"
48-
import { Task } from "../task"
50+
import { Task, cwd } from "../task"
51+
import { ClineRulesToggles } from "../../shared/cline-rules"
52+
import { refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
4953

5054
/*
5155
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -477,6 +481,10 @@ export class Controller {
477481
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
478482
this.postMessageToWebview({ type: "openAiModels", openAiModels })
479483
break
484+
case "refreshClineRules":
485+
await refreshClineRulesToggles(this.context, cwd)
486+
await this.postStateToWebview()
487+
break
480488
case "openImage":
481489
openImage(message.text!)
482490
break
@@ -654,6 +662,30 @@ export class Controller {
654662
}
655663
break
656664
}
665+
case "toggleClineRule": {
666+
const { isGlobal, rulePath, enabled } = message
667+
if (rulePath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
668+
if (isGlobal) {
669+
const toggles =
670+
((await getGlobalState(this.context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
671+
toggles[rulePath] = enabled
672+
await updateGlobalState(this.context, "globalClineRulesToggles", toggles)
673+
} else {
674+
const toggles =
675+
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
676+
toggles[rulePath] = enabled
677+
await updateWorkspaceState(this.context, "localClineRulesToggles", toggles)
678+
}
679+
await this.postStateToWebview()
680+
} else {
681+
console.error("toggleClineRule: Missing or invalid parameters", {
682+
rulePath,
683+
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
684+
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
685+
})
686+
}
687+
break
688+
}
657689
case "requestTotalTasksSize": {
658690
this.refreshTotalTasksSize()
659691
break
@@ -1872,8 +1904,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
18721904
mcpMarketplaceEnabled,
18731905
telemetrySetting,
18741906
planActSeparateModelsSetting,
1907+
globalClineRulesToggles,
18751908
} = await getAllExtensionState(this.context)
18761909

1910+
const localClineRulesToggles =
1911+
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
1912+
18771913
return {
18781914
version: this.context.extension?.packageJSON?.version ?? "",
18791915
apiConfiguration,
@@ -1896,6 +1932,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
18961932
telemetrySetting,
18971933
planActSeparateModelsSetting,
18981934
vscMachineId: vscode.env.machineId,
1935+
globalClineRulesToggles: globalClineRulesToggles || {},
1936+
localClineRulesToggles: localClineRulesToggles || {},
18991937
}
19001938
}
19011939

src/core/storage/state.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { BrowserSettings } from "../../shared/BrowserSettings"
1010
import { ChatSettings } from "../../shared/ChatSettings"
1111
import { TelemetrySetting } from "../../shared/TelemetrySetting"
1212
import { UserInfo } from "../../shared/UserInfo"
13-
import { ClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
13+
import { ClineRulesToggles } from "../../shared/cline-rules"
1414
/*
1515
Storage
1616
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
@@ -98,7 +98,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
9898
customInstructions,
9999
taskHistory,
100100
autoApprovalSettings,
101-
globalClineRulesToggles,
102101
browserSettings,
103102
chatSettings,
104103
vsCodeLmModelSelector,
@@ -123,6 +122,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
123122
sambanovaApiKey,
124123
planActSeparateModelsSettingRaw,
125124
favoritedModelIds,
125+
globalClineRulesToggles,
126126
] = await Promise.all([
127127
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
128128
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -169,7 +169,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
169169
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
170170
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
171171
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
172-
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
173172
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
174173
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
175174
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
@@ -194,6 +193,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
194193
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
195194
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
196195
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
196+
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
197197
])
198198

199199
let apiProvider: ApiProvider
@@ -210,6 +210,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
210210
}
211211
}
212212

213+
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
214+
213215
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
214216

215217
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
@@ -294,7 +296,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
294296
customInstructions,
295297
taskHistory,
296298
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
297-
clineRulesToggles: globalClineRulesToggles || {},
299+
globalClineRulesToggles: globalClineRulesToggles || {},
300+
localClineRulesToggles: localClineRulesToggles || {},
298301
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
299302
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
300303
userInfo,

src/core/task/index.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,20 @@ import {
7777
ensureTaskDirectoryExists,
7878
getSavedApiConversationHistory,
7979
getSavedClineMessages,
80-
GlobalFileNames,
8180
saveApiConversationHistory,
8281
saveClineMessages,
8382
} from "../storage/disk"
84-
import { McpHub } from "../../services/mcp/McpHub"
85-
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
8683
import {
87-
ClineRulesToggles,
8884
getGlobalClineRules,
8985
getLocalClineRules,
90-
synchronizeRuleToggles,
86+
refreshClineRulesToggles,
9187
} from "../context/instructions/user-instructions/cline-rules"
92-
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../storage/state"
88+
import { getGlobalState } from "../storage/state"
89+
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
90+
import { McpHub } from "../../services/mcp/McpHub"
9391

94-
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
92+
export const cwd =
93+
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
9594

9695
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
9796
type UserContent = Array<Anthropic.ContentBlockParam>
@@ -1286,19 +1285,12 @@ export class Task {
12861285
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
12871286
: ""
12881287

1289-
const globalClineRulesToggles =
1290-
((await getGlobalState(this.getContext(), "globalClineRulesToggles")) as ClineRulesToggles) || {}
1288+
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
1289+
12911290
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
1292-
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
1293-
await updateGlobalState(this.getContext(), "globalClineRulesToggles", updatedGlobalToggles)
1294-
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, updatedGlobalToggles)
1295-
1296-
const localClineRulesToggles =
1297-
((await getWorkspaceState(this.getContext(), "localClineRulesToggles")) as ClineRulesToggles) || {}
1298-
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
1299-
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
1300-
await updateWorkspaceState(this.getContext(), "localClineRulesToggles", updatedLocalToggles)
1301-
const localClineRulesFileInstructions = await getLocalClineRules(cwd, updatedLocalToggles)
1291+
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
1292+
1293+
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
13021294

13031295
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
13041296
let clineIgnoreInstructions: string | undefined

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { HistoryItem } from "./HistoryItem"
99
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
1010
import { TelemetrySetting } from "./TelemetrySetting"
1111
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
12+
import { ClineRulesToggles } from "./cline-rules"
1213

1314
// webview will hold state
1415
export interface ExtensionMessage {
@@ -147,6 +148,8 @@ export interface ExtensionState {
147148
}
148149
version: string
149150
vscMachineId: string
151+
globalClineRulesToggles: ClineRulesToggles
152+
localClineRulesToggles: ClineRulesToggles
150153
}
151154

152155
export interface ClineMessage {

src/shared/WebviewMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface WebviewMessage {
3333
| "refreshOpenRouterModels"
3434
| "refreshRequestyModels"
3535
| "refreshOpenAiModels"
36+
| "refreshClineRules"
3637
| "openMcpSettings"
3738
| "restartMcpServer"
3839
| "deleteMcpServer"
@@ -82,6 +83,8 @@ export interface WebviewMessage {
8283
| "searchFiles"
8384
| "toggleFavoriteModel"
8485
| "grpc_request"
86+
| "toggleClineRule"
87+
8588
// | "relaunchChromeDebugMode"
8689
text?: string
8790
uris?: string[] // Used for getRelativePaths
@@ -124,6 +127,10 @@ export interface WebviewMessage {
124127
message: any // JSON serialized protobuf message
125128
request_id: string // For correlating requests and responses
126129
}
130+
// For toggleClineRule
131+
isGlobal?: boolean
132+
rulePath?: string
133+
enabled?: boolean
127134
}
128135

129136
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

src/shared/cline-rules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
2626
import ContextMenu from "@/components/chat/ContextMenu"
2727
import { ChatSettings } from "@shared/ChatSettings"
2828
import ServersToggleModal from "./ServersToggleModal"
29+
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
2930

3031
interface ChatTextAreaProps {
3132
inputValue: string
@@ -1225,6 +1226,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12251226
</ButtonContainer>
12261227
</VSCodeButton>
12271228
<ServersToggleModal />
1229+
<ClineRulesToggleModal />
12281230

12291231
<ModelContainer ref={modelSelectorRef}>
12301232
<ModelButtonWrapper ref={buttonRef}>

webview-ui/src/components/chat/ServersToggleModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const ServersToggleModal: React.FC = () => {
7474
/>
7575

7676
<div className="flex justify-between items-center mb-2.5">
77-
<div className="m-0">MCP Servers</div>
77+
<div className="m-0 text-base font-semibold">MCP Servers</div>
7878
<VSCodeButton
7979
appearance="icon"
8080
onClick={() => {

0 commit comments

Comments
 (0)