Compare commits

...

8 Commits

Author SHA1 Message Date
abeatrix fe53ab914e feat(rules): add safe-dir check and refactor external toggles
- Skip processing rules when workspace is in home or Desktop via
  isSafeDirectory; return empty toggles for unsafe dirs to avoid
  unintended rule loading from sensitive locations
- Refactor external rules sync using typed configs (RuleSource/RuleConfig)
  and a syncRuleSource helper; combine Cursor rules from both sources
- Switch to node: imports (fs/promises, path, os) for clarity

This improves safety, code clarity, and maintainability while preserving
expected behavior for valid workspaces.
2025-11-13 03:39:37 -08:00
Saoud Rizwan 3881e3d2d5 Add AGENTS.md support 2025-11-12 21:34:07 -08:00
Saoud Rizwan ba6e1671cb fix: delete agents.md 2025-11-12 21:32:12 -08:00
Saoud Rizwan f215cadf2a docs: add support for AGENTS.md standard in Cline rules documentation 2025-11-12 21:12:04 -08:00
Saoud Rizwan e591c2af54 Update font size for documentation link in ClineRulesToggleModal component 2025-11-12 20:57:06 -08:00
Saoud Rizwan 899d334f0d Update webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 20:52:29 -08:00
Saoud Rizwan 859bf80ecb Update webview-ui/src/components/cline-rules/RuleRow.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 20:51:50 -08:00
Saoud Rizwan 01736423ac feat: add AGENTS.md support 2025-11-12 20:41:48 -08:00
19 changed files with 390 additions and 74 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add AGENTS.md support
+14
View File
@@ -81,6 +81,20 @@ your-project/
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### AGENTS.md Standard Support
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
your workspace root. This allows you to use the same rules file across different AI
coding tools.
```
your-project/
├── AGENTS.md
├── src/
└── ...
```
### Tips for Writing Effective Cline Rules
- Be Clear and Concise: Use simple language and avoid ambiguity.
+13 -2
View File
@@ -49,6 +49,9 @@ service FileService {
// Toggle a Windsurf rule (enable or disable)
rpc toggleWindsurfRule(ToggleWindsurfRuleRequest) returns (ClineRulesToggles);
// Toggle an Agents rule (enable or disable)
rpc toggleAgentsRule(ToggleAgentsRuleRequest) returns (ClineRulesToggles);
// Refreshes all rule toggles (Cline, External, and Workflows)
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
@@ -74,8 +77,9 @@ message RefreshedRules {
ClineRulesToggles local_cline_rules_toggles = 2;
ClineRulesToggles local_cursor_rules_toggles = 3;
ClineRulesToggles local_windsurf_rules_toggles = 4;
ClineRulesToggles local_workflow_toggles = 5;
ClineRulesToggles global_workflow_toggles = 6;
ClineRulesToggles local_agents_rules_toggles = 5;
ClineRulesToggles local_workflow_toggles = 6;
ClineRulesToggles global_workflow_toggles = 7;
}
// Request to toggle a Windsurf rule
@@ -85,6 +89,13 @@ message ToggleWindsurfRuleRequest {
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to toggle an Agents rule
message ToggleAgentsRuleRequest {
Metadata metadata = 1;
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to convert a list of URIs to relative paths
message RelativePathsRequest {
Metadata metadata = 1;
@@ -1,3 +1,6 @@
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import {
combineRuleToggles,
getRuleFilesTotalContent,
@@ -6,14 +9,53 @@ import {
} from "@core/context/instructions/user-instructions/rule-helpers"
import { formatResponse } from "@core/prompts/responses"
import { GlobalFileNames } from "@core/storage/disk"
import { listFiles } from "@services/glob/list-files"
import { ClineRulesToggles } from "@shared/cline-rules"
import { fileExistsAtPath, isDirectory } from "@utils/fs"
import fs from "fs/promises"
import path from "path"
import { Controller } from "@/core/controller"
// Types for better code clarity
type RuleSource = {
filePath: string
extension?: string
}
type RuleConfig = {
stateKey: "localWindsurfRulesToggles" | "localCursorRulesToggles" | "localAgentsRulesToggles"
sources: RuleSource[]
}
/**
* Refreshes the toggles for windsurf and cursor rules
* Check if a directory is a sensitive location (home directory or Desktop)
* Returns true if the directory is safe to process rules from
*/
function isSafeDirectory(workingDirectory: string): boolean {
const normalizedPath = path.resolve(workingDirectory)
const homeDir = os.homedir()
const desktopDir = path.join(homeDir, "Desktop")
// Don't process rules from home directory or Desktop
if (normalizedPath === homeDir || normalizedPath === desktopDir) {
return false
}
return true
}
/**
* Helper to synchronize a single rule source
*/
async function syncRuleSource(
workingDirectory: string,
source: RuleSource,
currentToggles: ClineRulesToggles,
): Promise<ClineRulesToggles> {
const fullPath = path.resolve(workingDirectory, source.filePath)
return await synchronizeRuleToggles(fullPath, currentToggles, source.extension)
}
/**
* Refreshes the toggles for windsurf, cursor, and agents rules
*/
export async function refreshExternalRulesToggles(
controller: Controller,
@@ -21,30 +63,86 @@ export async function refreshExternalRulesToggles(
): Promise<{
windsurfLocalToggles: ClineRulesToggles
cursorLocalToggles: ClineRulesToggles
agentsLocalToggles: ClineRulesToggles
}> {
// local windsurf toggles
const localWindsurfRulesToggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(workingDirectory)) {
// Return empty toggles for unsafe directories
return {
windsurfLocalToggles: {},
cursorLocalToggles: {},
agentsLocalToggles: {},
}
}
// local cursor toggles
const localCursorRulesToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const configs: Record<string, RuleConfig> = {
windsurf: {
stateKey: "localWindsurfRulesToggles",
sources: [{ filePath: GlobalFileNames.windsurfRules }],
},
cursor: {
stateKey: "localCursorRulesToggles",
sources: [
{ filePath: GlobalFileNames.cursorRulesDir, extension: ".mdc" },
{ filePath: GlobalFileNames.cursorRulesFile },
],
},
agents: {
stateKey: "localAgentsRulesToggles",
sources: [{ filePath: GlobalFileNames.agentsRulesFile }],
},
}
// cursor has two valid locations for rules files, so we need to check both and combine
// synchronizeRuleToggles will drop whichever rules files are not in each given path, but combining the results will result in no data loss
let localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesDir)
const updatedLocalCursorToggles1 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles, ".mdc")
// Process windsurf
const windsurfConfig = configs.windsurf
const windsurfToggles = controller.stateManager.getWorkspaceStateKey(windsurfConfig.stateKey)
const windsurfLocalToggles = await syncRuleSource(workingDirectory, windsurfConfig.sources[0], windsurfToggles)
controller.stateManager.setWorkspaceState(windsurfConfig.stateKey, windsurfLocalToggles)
localCursorRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.cursorRulesFile)
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
// Process cursor (combine results from both sources)
const cursorConfig = configs.cursor
const cursorToggles = controller.stateManager.getWorkspaceStateKey(cursorConfig.stateKey)
const [cursorToggles1, cursorToggles2] = await Promise.all([
syncRuleSource(workingDirectory, cursorConfig.sources[0], cursorToggles),
syncRuleSource(workingDirectory, cursorConfig.sources[1], cursorToggles),
])
const cursorLocalToggles = combineRuleToggles(cursorToggles1, cursorToggles2)
controller.stateManager.setWorkspaceState(cursorConfig.stateKey, cursorLocalToggles)
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
controller.stateManager.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
// Process agents
const agentsConfig = configs.agents
const agentsToggles = controller.stateManager.getWorkspaceStateKey(agentsConfig.stateKey)
const agentsLocalToggles = await syncRuleSource(workingDirectory, agentsConfig.sources[0], agentsToggles)
controller.stateManager.setWorkspaceState(agentsConfig.stateKey, agentsLocalToggles)
return {
windsurfLocalToggles: updatedLocalWindsurfToggles,
cursorLocalToggles: updatedLocalCursorToggles,
windsurfLocalToggles,
cursorLocalToggles,
agentsLocalToggles,
}
}
/**
* Helper to read a single rule file
*/
async function readRuleFile(filePath: string, toggles: ClineRulesToggles): Promise<string | undefined> {
// Check if file exists and is enabled
if (!(await fileExistsAtPath(filePath))) {
return undefined
}
if (await isDirectory(filePath)) {
return undefined
}
if (filePath in toggles && toggles[filePath] === false) {
return undefined
}
try {
const content = (await fs.readFile(filePath, "utf8")).trim()
return content || undefined
} catch (error) {
console.error(`Failed to read rule file at ${filePath}:`, error)
return undefined
}
}
@@ -52,68 +150,120 @@ export async function refreshExternalRulesToggles(
* Gather formatted windsurf rules
*/
export const getLocalWindsurfRules = async (cwd: string, toggles: ClineRulesToggles) => {
const windsurfRulesFilePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
let windsurfRulesFileInstructions: string | undefined
if (await fileExistsAtPath(windsurfRulesFilePath)) {
if (!(await isDirectory(windsurfRulesFilePath))) {
try {
if (windsurfRulesFilePath in toggles && toggles[windsurfRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(windsurfRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
windsurfRulesFileInstructions = formatResponse.windsurfRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .windsurfrules file at ${windsurfRulesFilePath}`)
}
}
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return undefined
}
return windsurfRulesFileInstructions
const filePath = path.resolve(cwd, GlobalFileNames.windsurfRules)
const content = await readRuleFile(filePath, toggles)
return content ? formatResponse.windsurfRulesLocalFileInstructions(cwd, content) : undefined
}
/**
* Gather formatted cursor rules, which can come from two sources
*/
export const getLocalCursorRules = async (cwd: string, toggles: ClineRulesToggles) => {
// we first check for the .cursorrules file
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return []
}
const results: (string | undefined)[] = []
// Check .cursorrules file
const cursorRulesFilePath = path.resolve(cwd, GlobalFileNames.cursorRulesFile)
let cursorRulesFileInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesFilePath)) {
if (!(await isDirectory(cursorRulesFilePath))) {
try {
if (cursorRulesFilePath in toggles && toggles[cursorRulesFilePath] !== false) {
const ruleFileContent = (await fs.readFile(cursorRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
cursorRulesFileInstructions = formatResponse.cursorRulesLocalFileInstructions(cwd, ruleFileContent)
}
}
} catch {
console.error(`Failed to read .cursorrules file at ${cursorRulesFilePath}`)
}
}
const fileContent = await readRuleFile(cursorRulesFilePath, toggles)
if (fileContent) {
results.push(formatResponse.cursorRulesLocalFileInstructions(cwd, fileContent))
}
// we then check for the .cursor/rules dir
// Check .cursor/rules directory
const cursorRulesDirPath = path.resolve(cwd, GlobalFileNames.cursorRulesDir)
let cursorRulesDirInstructions: string | undefined
if (await fileExistsAtPath(cursorRulesDirPath)) {
if (await isDirectory(cursorRulesDirPath)) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
cursorRulesDirInstructions = formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
}
} catch {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}`)
if ((await fileExistsAtPath(cursorRulesDirPath)) && (await isDirectory(cursorRulesDirPath))) {
try {
const rulesFilePaths = await readDirectoryRecursive(cursorRulesDirPath, ".mdc")
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
if (rulesFilesTotalContent) {
results.push(formatResponse.cursorRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent))
}
} catch (error) {
console.error(`Failed to read .cursor/rules directory at ${cursorRulesDirPath}:`, error)
}
}
return [cursorRulesFileInstructions, cursorRulesDirInstructions]
return results
}
/**
* Helper function to find all agents.md files recursively (case-insensitive)
* Only searches if a top-level agents.md file exists
*/
async function findAgentsMdFiles(cwd: string): Promise<string[]> {
// First check if top-level agents.md exists
const topLevelAgentsPath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
if (!(await fileExistsAtPath(topLevelAgentsPath))) {
return []
}
try {
// Search recursively for all agents.md files
const [allFiles] = await listFiles(cwd, true, 500)
const agentsFileName = GlobalFileNames.agentsRulesFile.toLowerCase()
return allFiles.filter((filePath) => path.basename(filePath).toLowerCase() === agentsFileName)
} catch (error) {
console.error(`Failed to find agents.md files in ${cwd}:`, error)
return []
}
}
/**
* Gather formatted agents rules - searches recursively and combines all agents.md files
*/
export const getLocalAgentsRules = async (cwd: string, toggles: ClineRulesToggles) => {
// Safety check: Don't process rules from home directory or Desktop
if (!isSafeDirectory(cwd)) {
return undefined
}
const agentsRulesFilePath = path.resolve(cwd, GlobalFileNames.agentsRulesFile)
// Check if the top-level agents.md file is enabled
if (agentsRulesFilePath in toggles && toggles[agentsRulesFilePath] === false) {
return undefined
}
try {
const agentsMdFiles = await findAgentsMdFiles(cwd)
if (agentsMdFiles.length === 0) {
return undefined
}
// Read and combine all agents.md files in parallel
const contentPromises = agentsMdFiles.map(async (filePath) => {
try {
const fullPath = path.resolve(cwd, filePath)
const content = (await fs.readFile(fullPath, "utf8")).trim()
if (!content) {
return null
}
const relativePath = path.relative(cwd, fullPath)
return `## ${relativePath}\n\n${content}`
} catch (error) {
console.error(`Failed to read agents.md file at ${filePath}:`, error)
return null
}
})
const contents = await Promise.all(contentPromises)
const combinedContent = contents.filter(Boolean).join("\n\n")
return combinedContent ? formatResponse.agentsRulesLocalFileInstructions(cwd, combinedContent) : undefined
} catch (error) {
console.error("Failed to read agents.md files:", error)
return undefined
}
}
@@ -299,6 +299,10 @@ export async function deleteRuleFile(
const toggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
delete toggles[rulePath]
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", toggles)
} else if (type === "agents") {
const toggles = controller.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
delete toggles[rulePath]
controller.stateManager.setWorkspaceState("localAgentsRulesToggles", toggles)
} else {
const toggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
delete toggles[rulePath]
+5 -1
View File
@@ -16,7 +16,10 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
try {
const cwd = await getCwd(getDesktopDir())
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller, cwd)
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller, cwd)
const { cursorLocalToggles, windsurfLocalToggles, agentsLocalToggles } = await refreshExternalRulesToggles(
controller,
cwd,
)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller, cwd)
return RefreshedRules.create({
@@ -24,6 +27,7 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
localClineRulesToggles: { toggles: localToggles },
localCursorRulesToggles: { toggles: cursorLocalToggles },
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
localAgentsRulesToggles: { toggles: agentsLocalToggles },
localWorkflowToggles: { toggles: localWorkflowToggles },
globalWorkflowToggles: { toggles: globalWorkflowToggles },
})
@@ -0,0 +1,33 @@
import type { ToggleAgentsRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import type { Controller } from "../index"
/**
* Toggles an Agents rule (enable or disable)
* @param controller The controller instance
* @param request The toggle request
* @returns The updated Agents rule toggles
*/
export async function toggleAgentsRule(controller: Controller, request: ToggleAgentsRuleRequest): Promise<ClineRulesToggles> {
const { rulePath, enabled } = request
if (!rulePath || typeof enabled !== "boolean") {
console.error("toggleAgentsRule: Missing or invalid parameters", {
rulePath,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
throw new Error("Missing or invalid parameters for toggleAgentsRule")
}
// Update the toggle in workspace state
const toggles = controller.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
toggles[rulePath] = enabled
controller.stateManager.setWorkspaceState("localAgentsRulesToggles", toggles)
// Get the current state to return in the response
const agentsToggles = controller.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
return ClineRulesToggles.create({
toggles: agentsToggles,
})
}
+2
View File
@@ -891,6 +891,7 @@ export class Controller {
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const localAgentsRulesToggles = this.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
const autoCondenseThreshold = this.stateManager.getGlobalSettingsKey("autoCondenseThreshold")
@@ -946,6 +947,7 @@ export class Controller {
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
localAgentsRulesToggles: localAgentsRulesToggles || {},
localWorkflowToggles: workflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
remoteRulesToggles: remoteRulesToggles,
+3
View File
@@ -249,6 +249,9 @@ Otherwise, if you have not completed the task and do not need additional informa
cursorRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .cursor/rules\n\nThe following is provided by a root-level .cursor/rules directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
agentsRulesLocalFileInstructions: (cwd: string, content: string) =>
`# AGENTS.md\n\nThe following is provided by AGENTS.md files found recursively throughout this working directory (${cwd.toPosix()}) where the user has specified instructions. Nested AGENTS.md will be combined below, and you should only apply the instructions for each AGENTS.md file that is directly applicable to the current task, i.e. if you are reading or writing to a file in that directory.\n\n${content}`,
fileContextWarning: (editedFiles: string[]): string => {
const fileCount = editedFiles.length
const fileVerb = fileCount === 1 ? "file has" : "files have"
@@ -15,6 +15,7 @@ export async function getUserInstructions(variant: PromptVariant, context: Syste
context.localCursorRulesFileInstructions,
context.localCursorRulesDirInstructions,
context.localWindsurfRulesFileInstructions,
context.localAgentsRulesFileInstructions,
context.clineIgnoreInstructions,
context.preferredLanguageInstructions,
)
@@ -37,6 +38,7 @@ function buildUserInstructions(
localCursorRulesFileInstructions?: string,
localCursorRulesDirInstructions?: string,
localWindsurfRulesFileInstructions?: string,
localAgentsRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
preferredLanguageInstructions?: string,
): string | undefined {
@@ -59,6 +61,9 @@ function buildUserInstructions(
if (localWindsurfRulesFileInstructions) {
customInstructions.push(localWindsurfRulesFileInstructions)
}
if (localAgentsRulesFileInstructions) {
customInstructions.push(localAgentsRulesFileInstructions)
}
if (clineIgnoreInstructions) {
customInstructions.push(clineIgnoreInstructions)
}
+1
View File
@@ -102,6 +102,7 @@ export interface SystemPromptContext {
readonly localCursorRulesFileInstructions?: string
readonly localCursorRulesDirInstructions?: string
readonly localWindsurfRulesFileInstructions?: string
readonly localAgentsRulesFileInstructions?: string
readonly clineIgnoreInstructions?: string
readonly preferredLanguageInstructions?: string
readonly browserSettings?: BrowserSettings
+1
View File
@@ -29,6 +29,7 @@ export const GlobalFileNames = {
cursorRulesDir: ".cursor/rules",
cursorRulesFile: ".cursorrules",
windsurfRules: ".windsurfrules",
agentsRulesFile: "AGENTS.md",
taskMetadata: "task_metadata.json",
mcpMarketplaceCatalog: "mcp_marketplace_catalog.json",
remoteConfig: (orgId: string) => `remote_config_${orgId}.json`,
+2
View File
@@ -153,12 +153,14 @@ export async function readWorkspaceStateFromDisk(context: ExtensionContext): Pro
const localClineRulesToggles = context.workspaceState.get("localClineRulesToggles") as ClineRulesToggles | undefined
const localWindsurfRulesToggles = context.workspaceState.get("localWindsurfRulesToggles") as ClineRulesToggles | undefined
const localCursorRulesToggles = context.workspaceState.get("localCursorRulesToggles") as ClineRulesToggles | undefined
const localAgentsRulesToggles = context.workspaceState.get("localAgentsRulesToggles") as ClineRulesToggles | undefined
const localWorkflowToggles = context.workspaceState.get("workflowToggles") as ClineRulesToggles | undefined
return {
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
localAgentsRulesToggles: localAgentsRulesToggles || {},
workflowToggles: localWorkflowToggles || {},
}
}
+8 -1
View File
@@ -13,6 +13,7 @@ import {
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import {
getLocalAgentsRules,
getLocalCursorRules,
getLocalWindsurfRules,
refreshExternalRulesToggles,
@@ -1985,7 +1986,10 @@ export class Task {
}
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.controller, this.cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.controller, this.cwd)
const { windsurfLocalToggles, cursorLocalToggles, agentsLocalToggles } = await refreshExternalRulesToggles(
this.controller,
this.cwd,
)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
@@ -1997,6 +2001,8 @@ export class Task {
)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(this.cwd, windsurfLocalToggles)
const localAgentsRulesFileInstructions = await getLocalAgentsRules(this.cwd, agentsLocalToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
if (clineIgnoreContent) {
@@ -2032,6 +2038,7 @@ export class Task {
localCursorRulesFileInstructions,
localCursorRulesDirInstructions,
localWindsurfRulesFileInstructions,
localAgentsRulesFileInstructions,
clineIgnoreInstructions,
preferredLanguageInstructions,
browserSettings: this.stateManager.getGlobalSettingsKey("browserSettings"),
+1
View File
@@ -81,6 +81,7 @@ export interface ExtensionState {
localWindsurfRulesToggles: ClineRulesToggles
remoteRulesToggles?: ClineRulesToggles
remoteWorkflowToggles?: ClineRulesToggles
localAgentsRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
strictPlanModeEnabled?: boolean
yoloModeToggled?: boolean
+1
View File
@@ -269,5 +269,6 @@ export interface LocalState {
localClineRulesToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
localAgentsRulesToggles: ClineRulesToggles
workflowToggles: ClineRulesToggles
}
@@ -3,6 +3,7 @@ import {
ClineRulesToggles,
RefreshedRules,
RuleScope,
ToggleAgentsRuleRequest,
ToggleClineRuleRequest,
ToggleCursorRuleRequest,
ToggleWindsurfRuleRequest,
@@ -25,6 +26,7 @@ const ClineRulesToggleModal: React.FC = () => {
localClineRulesToggles = {},
localCursorRulesToggles = {},
localWindsurfRulesToggles = {},
localAgentsRulesToggles = {},
localWorkflowToggles = {},
globalWorkflowToggles = {},
remoteRulesToggles = {},
@@ -34,6 +36,7 @@ const ClineRulesToggleModal: React.FC = () => {
setLocalClineRulesToggles,
setLocalCursorRulesToggles,
setLocalWindsurfRulesToggles,
setLocalAgentsRulesToggles,
setLocalWorkflowToggles,
setGlobalWorkflowToggles,
setRemoteRulesToggles,
@@ -64,6 +67,9 @@ const ClineRulesToggleModal: React.FC = () => {
if (response.localWindsurfRulesToggles?.toggles) {
setLocalWindsurfRulesToggles(response.localWindsurfRulesToggles.toggles)
}
if (response.localAgentsRulesToggles?.toggles) {
setLocalAgentsRulesToggles(response.localAgentsRulesToggles.toggles)
}
if (response.localWorkflowToggles?.toggles) {
setLocalWorkflowToggles(response.localWorkflowToggles.toggles)
}
@@ -95,6 +101,10 @@ const ClineRulesToggleModal: React.FC = () => {
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
const agentsRules = Object.entries(localAgentsRulesToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
const localWorkflows = Object.entries(localWorkflowToggles || {})
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
.sort(([a], [b]) => a.localeCompare(b))
@@ -172,6 +182,23 @@ const ClineRulesToggleModal: React.FC = () => {
})
}
const toggleAgentsRule = (rulePath: string, enabled: boolean) => {
FileServiceClient.toggleAgentsRule(
ToggleAgentsRuleRequest.create({
rulePath,
enabled,
} as ToggleAgentsRuleRequest),
)
.then((response: ClineRulesToggles) => {
if (response.toggles) {
setLocalAgentsRulesToggles(response.toggles)
}
})
.catch((error) => {
console.error("Error toggling Agents rule:", error)
})
}
const toggleWorkflow = (isGlobal: boolean, workflowPath: string, enabled: boolean) => {
FileServiceClient.toggleWorkflow(
ToggleWorkflowRequest.create({
@@ -330,7 +357,7 @@ const ClineRulesToggleModal: React.FC = () => {
<VSCodeLink
className="text-xs"
href="https://docs.cline.bot/features/cline-rules"
style={{ display: "inline" }}>
style={{ display: "inline", fontSize: "inherit" }}>
Docs
</VSCodeLink>
</p>
@@ -408,6 +435,7 @@ const ClineRulesToggleModal: React.FC = () => {
showNoRules={false}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
@@ -422,10 +450,19 @@ const ClineRulesToggleModal: React.FC = () => {
listGap="small"
rules={windsurfRules}
ruleType={"windsurf"}
showNewRule={true}
showNewRule={false}
showNoRules={false}
toggleRule={toggleWindsurfRule}
/>
<RulesToggleList
isGlobal={false}
listGap="small"
rules={agentsRules}
ruleType={"agents"}
showNewRule={true}
showNoRules={false}
toggleRule={toggleAgentsRule}
/>
</div>
</>
) : (
@@ -1,6 +1,7 @@
import { StringRequest } from "@shared/proto/cline/common"
import { RuleFileRequest } from "@shared/proto/index.cline"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { FileServiceClient } from "@/services/grpc-client"
const RuleRow: React.FC<{
@@ -57,6 +58,20 @@ const RuleRow: React.FC<{
</g>
</svg>
)
case "agents":
return (
<svg
height="16"
style={{ verticalAlign: "middle" }}
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5">
<circle cx="12" cy="8" r="3" />
<path d="M12 14c-4 0-6 2-6 4v2h12v-2c0-2-2-4-6-4z" />
</g>
</svg>
)
default:
return null
}
@@ -87,6 +102,18 @@ const RuleRow: React.FC<{
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
<span className="ph-no-capture">{finalDisplayName}</span>
{ruleType === "agents" && (
<Tooltip>
<TooltipTrigger asChild>
<span className="mt-1 ml-1.5 cursor-help">
<i className="codicon codicon-info" style={{ fontSize: "12px", opacity: 0.7 }} />
</span>
</TooltipTrigger>
<TooltipContent>
Searches recursively for all AGENTS.md files in the workspace when a top-level AGENTS.md exists
</TooltipContent>
</Tooltip>
)}
</span>
{/* Toggle Switch */}
@@ -73,6 +73,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
setLocalAgentsRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWorkflowToggles: (toggles: Record<string, boolean>) => void
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
setRemoteRulesToggles: (toggles: Record<string, boolean>) => void
@@ -203,6 +204,7 @@ export const ExtensionStateContextProvider: React.FC<{
localClineRulesToggles: {},
localCursorRulesToggles: {},
localWindsurfRulesToggles: {},
localAgentsRulesToggles: {},
localWorkflowToggles: {},
globalWorkflowToggles: {},
shellIntegrationTimeout: 4000,
@@ -679,6 +681,7 @@ export const ExtensionStateContextProvider: React.FC<{
localClineRulesToggles: state.localClineRulesToggles || {},
localCursorRulesToggles: state.localCursorRulesToggles || {},
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
localAgentsRulesToggles: state.localAgentsRulesToggles || {},
localWorkflowToggles: state.localWorkflowToggles || {},
globalWorkflowToggles: state.globalWorkflowToggles || {},
remoteRulesToggles: state.remoteRulesToggles || {},
@@ -736,6 +739,11 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
localWindsurfRulesToggles: toggles,
})),
setLocalAgentsRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localAgentsRulesToggles: toggles,
})),
setLocalWorkflowToggles: (toggles) =>
setState((prevState) => ({
...prevState,