mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d35f40179 | |||
| 17ef209cc0 | |||
| 3b24825429 | |||
| 852c7939e7 | |||
| e66c5f2535 | |||
| 5670a3009f | |||
| 70d683b6a9 | |||
| 88adc2cca3 | |||
| 81ca5337a6 | |||
| a1f7510ec7 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add modal UI for toggling Cline Rules
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "../../../storage/disk"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "../../../storage/disk"
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
|
||||
import { formatResponse } from "../../../prompts/responses"
|
||||
import fs from "fs/promises"
|
||||
|
||||
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
|
||||
import { ClineRulesToggles } from "../../../../shared/cline-rules"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../storage/state"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
@@ -147,3 +148,28 @@ export async function synchronizeRuleToggles(
|
||||
|
||||
return updatedToggles
|
||||
}
|
||||
|
||||
export async function refreshClineRulesToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<{
|
||||
globalToggles: ClineRulesToggles
|
||||
localToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global toggles
|
||||
const globalClineRulesToggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
|
||||
await updateGlobalState(context, "globalClineRulesToggles", updatedGlobalToggles)
|
||||
|
||||
// Local toggles
|
||||
const localClineRulesToggles = ((await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
|
||||
await updateWorkspaceState(context, "localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
globalToggles: updatedGlobalToggles,
|
||||
localToggles: updatedLocalToggles,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,16 @@ import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
getSecret,
|
||||
getWorkspaceState,
|
||||
resetExtensionState,
|
||||
storeSecret,
|
||||
updateApiConfiguration,
|
||||
updateGlobalState,
|
||||
updateWorkspaceState,
|
||||
} from "../storage/state"
|
||||
import { Task } from "../task"
|
||||
import { Task, cwd } from "../task"
|
||||
import { ClineRulesToggles } from "../../shared/cline-rules"
|
||||
import { refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -470,6 +474,10 @@ export class Controller {
|
||||
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
|
||||
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openImage":
|
||||
openImage(message.text!)
|
||||
break
|
||||
@@ -647,6 +655,30 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleClineRule": {
|
||||
const { isGlobal, rulePath, enabled } = message
|
||||
if (rulePath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
|
||||
if (isGlobal) {
|
||||
const toggles =
|
||||
((await getGlobalState(this.context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateGlobalState(this.context, "globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
toggles[rulePath] = enabled
|
||||
await updateWorkspaceState(this.context, "localClineRulesToggles", toggles)
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
console.error("toggleClineRule: Missing or invalid parameters", {
|
||||
rulePath,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
@@ -1865,8 +1897,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
globalClineRulesToggles,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const localClineRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
apiConfiguration,
|
||||
@@ -1889,6 +1925,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
vscMachineId: vscode.env.machineId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { BrowserSettings } from "../../shared/BrowserSettings"
|
||||
import { ChatSettings } from "../../shared/ChatSettings"
|
||||
import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { UserInfo } from "../../shared/UserInfo"
|
||||
import { ClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
|
||||
import { ClineRulesToggles } from "../../shared/cline-rules"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -98,7 +98,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
globalClineRulesToggles,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
vsCodeLmModelSelector,
|
||||
@@ -123,6 +122,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sambanovaApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
globalClineRulesToggles,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
@@ -169,7 +169,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
|
||||
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
|
||||
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
|
||||
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
|
||||
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
@@ -194,6 +193,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -210,6 +210,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
|
||||
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
|
||||
|
||||
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
|
||||
@@ -294,7 +296,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
clineRulesToggles: globalClineRulesToggles || {},
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
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)
|
||||
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
|
||||
userInfo,
|
||||
|
||||
+11
-19
@@ -77,21 +77,20 @@ import {
|
||||
ensureTaskDirectoryExists,
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
GlobalFileNames,
|
||||
saveApiConversationHistory,
|
||||
saveClineMessages,
|
||||
} from "../storage/disk"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import {
|
||||
ClineRulesToggles,
|
||||
getGlobalClineRules,
|
||||
getLocalClineRules,
|
||||
synchronizeRuleToggles,
|
||||
refreshClineRulesToggles,
|
||||
} from "../context/instructions/user-instructions/cline-rules"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../storage/state"
|
||||
import { getGlobalState } from "../storage/state"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
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
|
||||
export 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
|
||||
|
||||
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
@@ -1286,19 +1285,12 @@ export class Task {
|
||||
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
|
||||
: ""
|
||||
|
||||
const globalClineRulesToggles =
|
||||
((await getGlobalState(this.getContext(), "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
|
||||
await updateGlobalState(this.getContext(), "globalClineRulesToggles", updatedGlobalToggles)
|
||||
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, updatedGlobalToggles)
|
||||
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
|
||||
|
||||
const localClineRulesToggles =
|
||||
((await getWorkspaceState(this.getContext(), "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles)
|
||||
await updateWorkspaceState(this.getContext(), "localClineRulesToggles", updatedLocalToggles)
|
||||
const localClineRulesFileInstructions = await getLocalClineRules(cwd, updatedLocalToggles)
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
|
||||
|
||||
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
|
||||
|
||||
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
|
||||
let clineIgnoreInstructions: string | undefined
|
||||
|
||||
@@ -9,6 +9,7 @@ import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
|
||||
import { ClineRulesToggles } from "./cline-rules"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -146,6 +147,8 @@ export interface ExtensionState {
|
||||
}
|
||||
version: string
|
||||
vscMachineId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface WebviewMessage {
|
||||
| "refreshOpenRouterModels"
|
||||
| "refreshRequestyModels"
|
||||
| "refreshOpenAiModels"
|
||||
| "refreshClineRules"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
@@ -81,6 +82,8 @@ export interface WebviewMessage {
|
||||
| "searchFiles"
|
||||
| "toggleFavoriteModel"
|
||||
| "grpc_request"
|
||||
| "toggleClineRule"
|
||||
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
uris?: string[] // Used for getRelativePaths
|
||||
@@ -123,6 +126,10 @@ export interface WebviewMessage {
|
||||
message: any // JSON serialized protobuf message
|
||||
request_id: string // For correlating requests and responses
|
||||
}
|
||||
// For toggleClineRule
|
||||
isGlobal?: boolean
|
||||
rulePath?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type ClineRulesToggles = Record<string, boolean> // filepath -> enabled/disabled
|
||||
@@ -26,6 +26,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "@/components/chat/ChatView"
|
||||
import ContextMenu from "@/components/chat/ContextMenu"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import ServersToggleModal from "./ServersToggleModal"
|
||||
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
@@ -1225,6 +1226,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
</ButtonContainer>
|
||||
</VSCodeButton>
|
||||
<ServersToggleModal />
|
||||
<ClineRulesToggleModal />
|
||||
|
||||
<ModelContainer ref={modelSelectorRef}>
|
||||
<ModelButtonWrapper ref={buttonRef}>
|
||||
|
||||
@@ -74,7 +74,7 @@ const ServersToggleModal: React.FC = () => {
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-2.5">
|
||||
<div className="m-0">MCP Servers</div>
|
||||
<div className="m-0 text-base font-semibold">MCP Servers</div>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useRef, useState, useEffect } from "react"
|
||||
import { useClickAway, useWindowSize } from "react-use"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import RulesToggleList from "./RulesToggleList"
|
||||
|
||||
const ClineRulesToggleModal: React.FC = () => {
|
||||
const { globalClineRulesToggles = {}, localClineRulesToggles = {} } = useExtensionState()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
vscode.postMessage({ type: "refreshClineRules" })
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
// Format global rules for display with proper typing
|
||||
const globalRules = Object.entries(globalClineRulesToggles || {})
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
// Format local rules for display with proper typing
|
||||
const localRules = Object.entries(localClineRulesToggles || {})
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
// Handle toggle rule
|
||||
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
|
||||
vscode.postMessage({
|
||||
type: "toggleClineRule",
|
||||
isGlobal,
|
||||
rulePath,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
useClickAway(modalRef, () => {
|
||||
setIsVisible(false)
|
||||
})
|
||||
|
||||
// Calculate positions for modal and arrow
|
||||
useEffect(() => {
|
||||
if (isVisible && buttonRef.current) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect()
|
||||
const buttonCenter = buttonRect.left + buttonRect.width / 2
|
||||
const rightPosition = document.documentElement.clientWidth - buttonCenter - 5
|
||||
|
||||
setArrowPosition(rightPosition)
|
||||
setMenuPosition(buttonRect.top + 1)
|
||||
}
|
||||
}, [isVisible, viewportWidth, viewportHeight])
|
||||
|
||||
return (
|
||||
<div ref={modalRef}>
|
||||
<div ref={buttonRef} className="inline-flex min-w-0 max-w-full">
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Cline Rules"
|
||||
onClick={() => setIsVisible(!isVisible)}
|
||||
style={{ padding: "0px 0px", height: "20px" }}>
|
||||
<div className="flex items-center gap-1 text-xs whitespace-nowrap min-w-0 w-full">
|
||||
<span className="codicon codicon-law flex items-center" style={{ fontSize: "12.5px", marginBottom: 1 }} />
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<div
|
||||
className="fixed left-[15px] right-[15px] border border-[var(--vscode-editorGroup-border)] p-3 rounded z-[1000] overflow-y-auto"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
overscrollBehavior: "contain",
|
||||
}}>
|
||||
<div
|
||||
className="fixed w-[10px] h-[10px] z-[-1] rotate-45 border-r border-b border-[var(--vscode-editorGroup-border)]"
|
||||
style={{
|
||||
bottom: `calc(100vh - ${menuPosition}px)`,
|
||||
right: arrowPosition,
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-2.5">
|
||||
<div className="m-0 text-base font-semibold">Cline Rules</div>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
})
|
||||
setIsVisible(false)
|
||||
}}>
|
||||
{/* <span className="codicon codicon-gear text-[10px]"></span> */}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{/* Global Rules Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Rules</div>
|
||||
<RulesToggleList
|
||||
rules={globalRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
|
||||
listGap="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Rules Section */}
|
||||
<div style={{ marginBottom: -10 }}>
|
||||
<div className="text-sm font-normal mb-2">Workspace Rules</div>
|
||||
<RulesToggleList
|
||||
rules={localRules}
|
||||
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
|
||||
listGap="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClineRulesToggleModal
|
||||
@@ -0,0 +1,49 @@
|
||||
const RuleRow: React.FC<{
|
||||
rulePath: string
|
||||
enabled: boolean
|
||||
toggleRule: (rulePath: string, enabled: boolean) => void
|
||||
}> = ({ rulePath, enabled, toggleRule }) => {
|
||||
// Get the filename from the path for display
|
||||
const displayName = rulePath.split("/").pop() || rulePath
|
||||
|
||||
return (
|
||||
<div className="mb-2.5">
|
||||
<div
|
||||
className={`flex items-center p-2 rounded bg-[var(--vscode-textCodeBlock-background)] ${
|
||||
enabled ? "opacity-100" : "opacity-60"
|
||||
}`}>
|
||||
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
|
||||
{displayName}
|
||||
</span>
|
||||
|
||||
{/* Toggle Switch */}
|
||||
<div className="flex items-center ml-2">
|
||||
<div
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
tabIndex={0}
|
||||
className={`w-[20px] h-[10px] rounded-[5px] relative cursor-pointer transition-colors duration-200 ${
|
||||
enabled
|
||||
? "bg-[var(--vscode-testing-iconPassed)] opacity-90"
|
||||
: "bg-[var(--vscode-titleBar-inactiveForeground)] opacity-50"
|
||||
}`}
|
||||
onClick={() => toggleRule(rulePath, !enabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
toggleRule(rulePath, !enabled)
|
||||
}
|
||||
}}>
|
||||
<div
|
||||
className={`w-[6px] h-[6px] bg-white border border-[#66666699] rounded-full absolute top-[1px] transition-all duration-200 ${
|
||||
enabled ? "left-[12px]" : "left-[2px]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RuleRow
|
||||
@@ -0,0 +1,31 @@
|
||||
import RuleRow from "./RuleRow"
|
||||
|
||||
const RulesToggleList = ({
|
||||
rules,
|
||||
toggleRule,
|
||||
listGap = "medium",
|
||||
}: {
|
||||
rules: [string, boolean][]
|
||||
toggleRule: (rulePath: string, enabled: boolean) => void
|
||||
listGap?: "small" | "medium" | "large"
|
||||
}) => {
|
||||
const gapClasses = {
|
||||
small: "gap-0",
|
||||
medium: "gap-2.5",
|
||||
large: "gap-5",
|
||||
}
|
||||
|
||||
const gapClass = gapClasses[listGap]
|
||||
|
||||
return rules.length > 0 ? (
|
||||
<div className={`flex flex-col ${gapClass}`}>
|
||||
{rules.map(([rulePath, enabled]) => (
|
||||
<RuleRow key={rulePath} rulePath={rulePath} enabled={enabled} toggleRule={toggleRule} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 my-5 text-[var(--vscode-descriptionForeground)]">No rules found</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RulesToggleList
|
||||
@@ -17,7 +17,7 @@ import { vscode } from "../utils/vscode"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
showWelcome: boolean
|
||||
@@ -53,6 +53,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
telemetrySetting: "unset",
|
||||
vscMachineId: "",
|
||||
planActSeparateModelsSetting: true,
|
||||
globalClineRulesToggles: {},
|
||||
localClineRulesToggles: {},
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -128,6 +130,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "openRouterModels": {
|
||||
const updatedModels = message.openRouterModels ?? {}
|
||||
setOpenRouterModels({
|
||||
@@ -184,6 +187,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
mcpMarketplaceCatalog,
|
||||
filePaths,
|
||||
totalTasksSize,
|
||||
globalClineRulesToggles: state.globalClineRulesToggles || {},
|
||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
|
||||
Reference in New Issue
Block a user