Compare commits

...

10 Commits

Author SHA1 Message Date
celestial-vault 9d35f40179 refresh cline rules on modal open 2025-04-16 19:16:08 -07:00
celestial-vault 17ef209cc0 fix duplicate globalClineRulesToggles declaration in state.ts from merge 2025-04-16 17:40:29 -07:00
celestial-vault 3b24825429 fix bad merged files 2025-04-16 17:36:32 -07:00
celestial-vault 852c7939e7 merge conflicts 2025-04-16 13:19:39 -07:00
celestial-vault e66c5f2535 change codicon 2025-04-16 11:06:35 -07:00
celestial-vault 5670a3009f changeset 2025-04-15 17:18:52 -07:00
celestial-vault 70d683b6a9 add toggles modal 2025-04-15 17:18:18 -07:00
celestial-vault 88adc2cca3 merge conflicts 2025-04-14 16:41:27 -07:00
celestial-vault 81ca5337a6 add toggle functionality to clinerules 2025-04-14 15:11:35 -07:00
celestial-vault a1f7510ec7 add fetching global cline rules files 2025-04-12 16:30:33 -07:00
14 changed files with 324 additions and 29 deletions
+5
View File
@@ -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,
}
}
+39 -1
View File
@@ -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 || {},
}
}
+7 -4
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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 {
+7
View File
@@ -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"
+1
View File
@@ -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,