Compare commits

...
Author SHA1 Message Date
celestial-vault a9fe3c5a7e tighten validation 2025-04-18 12:37:50 -07:00
celestial-vault fe55c5afb2 update placeholder 2025-04-18 12:21:59 -07:00
celestial-vault 27c0c97f88 remove commented out code 2025-04-18 12:18:45 -07:00
celestial-vault 6307d4c3e4 fix merge issues causing duplicates 2025-04-18 12:13:59 -07:00
celestial-vault 4156af1250 fix missing boolean check 2025-04-18 12:05:05 -07:00
celestial-vault 295e93bb82 merge conflicts 2025-04-18 12:03:49 -07:00
celestial-vault a638d19d0c changeset 2025-04-18 11:54:03 -07:00
celestial-vault 100d2ed104 add create new rule row to modal 2025-04-18 11:53:33 -07:00
8 changed files with 231 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add a row to the Cline Rules modal to create a new rule file
@@ -174,6 +174,32 @@ export async function refreshClineRulesToggles(
}
}
export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: string) => {
try {
let filePath: string
if (isGlobal) {
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
filePath = path.join(globalClineRulesFilePath, filename)
} else {
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
await fs.mkdir(localClineRulesFilePath, { recursive: true })
filePath = path.join(localClineRulesFilePath, filename)
}
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
return { filePath, fileExists }
}
await fs.writeFile(filePath, "", "utf8")
return { filePath, fileExists: false }
} catch (error) {
return { filePath: null, fileExists: false }
}
}
export async function deleteRuleFile(
context: vscode.ExtensionContext,
rulePath: string,
+31 -1
View File
@@ -49,7 +49,7 @@ import {
} from "../storage/state"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "../../shared/cline-rules"
import { deleteRuleFile, refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
import { createRuleFile, deleteRuleFile, 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
@@ -449,6 +449,36 @@ export class Controller {
break
case "openFile":
openFile(message.text!)
break
case "createRuleFile":
if (typeof message.isGlobal !== "boolean" || typeof message.filename !== "string" || !message.filename) {
console.error("createRuleFile: Missing or invalid parameters", {
isGlobal:
typeof message.isGlobal === "boolean" ? message.isGlobal : `Invalid: ${typeof message.isGlobal}`,
filename: typeof message.filename === "string" ? message.filename : `Invalid: ${typeof message.filename}`,
})
return
}
const { filePath, fileExists } = await createRuleFile(message.isGlobal, message.filename, cwd)
if (fileExists && filePath) {
vscode.window.showWarningMessage(`Rule file "${message.filename}" already exists.`)
// Still open it for editing
openFile(filePath)
return
} else if (filePath && !fileExists) {
await refreshClineRulesToggles(this.context, cwd)
await this.postStateToWebview()
openFile(filePath)
vscode.window.showInformationMessage(
`Created new ${message.isGlobal ? "global" : "workspace"} rule file: ${message.filename}`,
)
} else {
// null filePath
vscode.window.showErrorMessage(`Failed to create rule file.`)
}
break
case "openMention":
openMention(message.text)
+4 -1
View File
@@ -27,6 +27,7 @@ export interface WebviewMessage {
| "openImage"
| "openInBrowser"
| "openFile"
| "createRuleFile"
| "openMention"
| "cancelTask"
| "showChatView"
@@ -126,10 +127,12 @@ export interface WebviewMessage {
message: any // JSON serialized protobuf message
request_id: string // For correlating requests and responses
}
// For toggleClineRule
// For cline rules
isGlobal?: boolean
rulePath?: string
enabled?: boolean
filename?: string
offset?: number
}
@@ -100,9 +100,7 @@ const ClineRulesToggleModal: React.FC = () => {
type: "openExtensionSettings",
})
setIsVisible(false)
}}>
{/* <span className="codicon codicon-gear text-[10px]"></span> */}
</VSCodeButton>
}}></VSCodeButton>
</div>
{/* Global Rules Section */}
@@ -111,8 +109,8 @@ const ClineRulesToggleModal: React.FC = () => {
<RulesToggleList
rules={globalRules}
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
isGlobal={true}
listGap="small"
isGlobal={true}
/>
</div>
@@ -122,8 +120,8 @@ const ClineRulesToggleModal: React.FC = () => {
<RulesToggleList
rules={localRules}
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
isGlobal={false}
listGap="small"
isGlobal={false}
/>
</div>
</div>
@@ -0,0 +1,136 @@
import { useState, useRef, useEffect } from "react"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface NewRuleRowProps {
isGlobal: boolean // To determine where to create the file
}
const NewRuleRow: React.FC<NewRuleRowProps> = ({ isGlobal }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [filename, setFilename] = useState("")
const inputRef = useRef<HTMLInputElement>(null)
const [error, setError] = useState<string | null>(null)
// Focus the input when expanded
useEffect(() => {
if (isExpanded && inputRef.current) {
inputRef.current.focus()
}
}, [isExpanded])
const getExtension = (filename: string): string => {
if (filename.startsWith(".") && !filename.includes(".", 1)) return ""
const match = filename.match(/\.[^.]+$/)
return match ? match[0].toLowerCase() : ""
}
const isValidExtension = (ext: string): boolean => {
// Valid if it's empty (no extension) or .md or .txt
return ext === "" || ext === ".md" || ext === ".txt"
}
const handleCreateRule = () => {
if (filename.trim()) {
const trimmedFilename = filename.trim()
const extension = getExtension(trimmedFilename)
if (!isValidExtension(extension)) {
setError("Only .md, .txt, or no file extension allowed")
return
}
let finalFilename = trimmedFilename
if (extension === "") {
finalFilename = `${trimmedFilename}.md`
}
vscode.postMessage({
type: "createRuleFile",
isGlobal,
filename: finalFilename,
})
setFilename("")
setError(null)
setIsExpanded(false)
}
}
const handleBlur = () => {
setIsExpanded(false)
setError(null)
setFilename("")
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleCreateRule()
} else if (e.key === "Escape") {
setIsExpanded(false)
setFilename("")
}
}
return (
<div
className={`mb-2.5 transition-all duration-300 ease-in-out ${isExpanded ? "opacity-100" : "opacity-70 hover:opacity-100"}`}
onClick={() => !isExpanded && setIsExpanded(true)}>
<div
className={`flex items-center p-2 rounded bg-[var(--vscode-input-background)] transition-all duration-300 ease-in-out h-[18px] ${
isExpanded ? "shadow-sm" : ""
}`}>
{isExpanded ? (
<>
<input
ref={inputRef}
type="text"
placeholder="rule-name (.md, .txt, or no extension)"
value={filename}
onChange={(e) => setFilename(e.target.value)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className="flex-1 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border-0 outline-0 rounded focus:outline-none focus:ring-0 focus:border-transparent"
style={{
outline: "none",
}}
/>
<div className="flex items-center ml-2 space-x-2">
<VSCodeButton
appearance="icon"
aria-label="Create rule file"
title="Create rule file"
onClick={handleCreateRule}
style={{ padding: "0px" }}>
<span className="codicon codicon-add text-[14px]" />
</VSCodeButton>
</div>
</>
) : (
<>
<span className="flex-1 text-[var(--vscode-descriptionForeground)] bg-[var(--vscode-input-background)] italic text-xs">
New rule file...
</span>
<div className="flex items-center ml-2 space-x-2">
<VSCodeButton
appearance="icon"
aria-label="New rule file"
title="New rule file"
onClick={(e) => {
e.stopPropagation()
setIsExpanded(true)
}}
style={{ padding: "0px" }}>
<span className="codicon codicon-add text-[14px]" />
</VSCodeButton>
</div>
</>
)}
</div>
{isExpanded && error && <div className="text-[var(--vscode-errorForeground)] text-xs mt-1 ml-2">{error}</div>}
</div>
)
}
export default NewRuleRow
@@ -28,7 +28,7 @@ const RuleRow: React.FC<{
return (
<div className="mb-2.5">
<div
className={`flex items-center p-2 rounded bg-[var(--vscode-textCodeBlock-background)] ${
className={`flex items-center p-2 rounded bg-[var(--vscode-textCodeBlock-background)] h-[18px] ${
enabled ? "opacity-100" : "opacity-60"
}`}>
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
@@ -1,15 +1,16 @@
import NewRuleRow from "./NewRuleRow"
import RuleRow from "./RuleRow"
const RulesToggleList = ({
rules,
toggleRule,
isGlobal,
listGap = "medium",
isGlobal,
}: {
rules: [string, boolean][]
toggleRule: (rulePath: string, enabled: boolean) => void
isGlobal: boolean
listGap?: "small" | "medium" | "large"
isGlobal: boolean
}) => {
const gapClasses = {
small: "gap-0",
@@ -19,14 +20,30 @@ const RulesToggleList = ({
const gapClass = gapClasses[listGap]
return rules.length > 0 ? (
return (
<div className={`flex flex-col ${gapClass}`}>
{rules.map(([rulePath, enabled]) => (
<RuleRow key={rulePath} rulePath={rulePath} enabled={enabled} isGlobal={isGlobal} toggleRule={toggleRule} />
))}
{rules.length > 0 ? (
<>
{rules.map(([rulePath, enabled]) => (
<RuleRow
key={rulePath}
rulePath={rulePath}
enabled={enabled}
isGlobal={isGlobal}
toggleRule={toggleRule}
/>
))}
<NewRuleRow isGlobal={isGlobal} />
</>
) : (
<>
<div className="flex flex-col items-center gap-3 my-3 text-[var(--vscode-descriptionForeground)]">
No rules found
</div>
<NewRuleRow isGlobal={isGlobal} />
</>
)}
</div>
) : (
<div className="flex flex-col items-center gap-3 my-5 text-[var(--vscode-descriptionForeground)]">No rules found</div>
)
}