Repository: cline/cline
Pull request #2973: MOAR RULES
Author: login: celestial-vault
Created: 2025-04-18T18:59:25Z | Merged: 2025-04-18T23:39:57Z

### Description

Add a row to create new rule files to the Cline Rules Modal.

Will default to .md files if no extension in included. Only supports .txt and .md extensions, as the rule file should be just text.

Handles other extensions by displaying an error.

### Test Procedure

Tested:
File creation
Weird filenames
Using other extensions
UI edge cases (state is cleared properly, etc.)
Duplicate filenames
Backend errors are handled gracefully

### Type of Change

<!-- Put an 'x' in all boxes that apply -->

-   [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
-   [x] ✨ New feature (non-breaking change which adds functionality)
-   [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
-   [ ] ♻️ Refactor Changes
-   [ ] 💅 Cosmetic Changes
-   [ ] 📚 Documentation update
-   [ ] 🏃 Workflow Changes

### Pre-flight Checklist

<!-- Put an 'x' in all boxes that apply -->

-   [x] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
-   [x] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`)
-   [x] I have created a changeset using `npm run changeset` (required for user-facing changes)
-   [x] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)

### Screenshots
#### Valid use
https://github.com/user-attachments/assets/7da8f6c7-301b-4d71-a792-3f84d76a30cf

#### Invalid use

https://github.com/user-attachments/assets/e1cf5167-41cb-456d-939d-cee745ce0b9b





### Additional Notes

<!-- Add any additional notes for reviewers -->

<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Add functionality to create new rule files in the Cline Rules Modal, with support for `.md` and `.txt` extensions and error handling for others.
> 
>   - **Behavior**:
>     - Adds `createRuleFile` function in `cline-rules.ts` to create new rule files, defaulting to `.md` if no extension is provided.
>     - Handles `.txt` and `.md` extensions; other extensions trigger an error.
>     - Updates `handleWebviewMessage` in `index.ts` to handle `createRuleFile` messages.
>   - **UI Components**:
>     - Adds `NewRuleRow` component for creating new rule files in `ClineRulesToggleModal.tsx`.
>     - Updates `RulesToggleList` to include `NewRuleRow` for both global and local rules.
>     - Modifies `RuleRow` to include delete functionality via `deleteClineRule` message.
>   - **Webview Messaging**:
>     - Adds `createRuleFile` and `deleteClineRule` message types in `WebviewMessage.ts`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=cline%2Fcline&utm_source=github&utm_medium=referral)<sup> for a9fe3c5a7eb26d38dd5bbdc175415e78676389d5. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/.changeset/fresh-cups-tap.md b/.changeset/fresh-cups-tap.md
new file mode 100644
index 00000000000..e4dcf45d50f
--- /dev/null
+++ b/.changeset/fresh-cups-tap.md
@@ -0,0 +1,5 @@
+---
+"claude-dev": minor
+---
+
+Add a row to the Cline Rules modal to create a new rule file
diff --git a/src/core/context/instructions/user-instructions/cline-rules.ts b/src/core/context/instructions/user-instructions/cline-rules.ts
index 3f19c54b7a1..d636f346b43 100644
--- a/src/core/context/instructions/user-instructions/cline-rules.ts
+++ b/src/core/context/instructions/user-instructions/cline-rules.ts
@@ -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,
diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts
index 93d2eccc5fa..dce41a76858 100644
--- a/src/core/controller/index.ts
+++ b/src/core/controller/index.ts
@@ -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)
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 8c14847a89f..ee7d2553d8a 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -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
 }
 
diff --git a/webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx b/webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx
index fb07f6c737f..423ff6b9b63 100644
--- a/webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx
+++ b/webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx
@@ -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>
diff --git a/webview-ui/src/components/cline-rules/NewRuleRow.tsx b/webview-ui/src/components/cline-rules/NewRuleRow.tsx
new file mode 100644
index 00000000000..c44dc91335f
--- /dev/null
+++ b/webview-ui/src/components/cline-rules/NewRuleRow.tsx
@@ -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
diff --git a/webview-ui/src/components/cline-rules/RuleRow.tsx b/webview-ui/src/components/cline-rules/RuleRow.tsx
index 3f43fd2d865..b2994579da9 100644
--- a/webview-ui/src/components/cline-rules/RuleRow.tsx
+++ b/webview-ui/src/components/cline-rules/RuleRow.tsx
@@ -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}>
diff --git a/webview-ui/src/components/cline-rules/RulesToggleList.tsx b/webview-ui/src/components/cline-rules/RulesToggleList.tsx
index 930ceb24581..cebe198d5c0 100644
--- a/webview-ui/src/components/cline-rules/RulesToggleList.tsx
+++ b/webview-ui/src/components/cline-rules/RulesToggleList.tsx
@@ -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>
 	)
 }
 


Reviews:
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Reviewed everything up to a638d19d0ce56cbff48401a44892f9e66945ee8c in <ellipsistime>2 minutes and 16 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `348` lines of code in `8` files
 - Skipped `0` files when reviewing.
 - Skipped posting `6` drafted comments based on config settings.


<details>
<summary>1. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:22</code></summary>

- **Draft comment:** 
The getExtension function uses a regex to extract the extension. Consider adding a comment explaining its behavior and edge cases (e.g. filenames beginning with a dot without further dots).
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:48</code></summary>

- **Draft comment:** 
When posting the createRuleFile message, error handling is only done on the client-side state. Consider handling failure responses from the extension if rule creation fails.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>webview-ui/src/components/cline-rules/RulesToggleList.tsx:27</code></summary>

- **Draft comment:** 
The component maps over rules and appends a NewRuleRow. This pattern works well, but consider adding a fallback for when rules array might be empty, as is partially handled below.
- **Reason this comment was not posted:** 
Confidence changes required: `30%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>webview-ui/src/components/cline-rules/RuleRow.tsx:25</code></summary>

- **Draft comment:** 
Good usage of accessible role and keyboard handlers for the toggle switch. Confirm that touch targets are large enough for users.
- **Reason this comment was not posted:** 
Confidence changes required: `20%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx:18</code></summary>

- **Draft comment:** 
The useEffect that posts a 'refreshClineRules' message when the modal becomes visible is effective. Ensure that dependency array includes all necessary variables to avoid stale closures.
- **Reason this comment was not posted:** 
Confidence changes required: `30%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>src/shared/WebviewMessage.ts:35</code></summary>

- **Draft comment:** 
It appears that 'refreshRequestyModels' may contain a typographical error. Consider reviewing if it should be 'refreshRequestModels' or possibly 'refreshRequestedModels' for consistency with the naming convention.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>


Workflow ID: <workflowid>`wflow_AfyioV2aMEG35bu2`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can teach Ellipsis what to look for with :+1: / :-1: [feedback](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:01:47Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on 4156af12508b7a1dd230d106bf7902b16fd84ad2 in <ellipsistime>1 minute and 45 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `13` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `1` drafted comments based on config settings.


<details>
<summary>1. <code>src/core/controller/index.ts:454</code></summary>

- **Draft comment:** 
Using a type-check for 'message.isGlobal' avoids rejecting false values. This is more robust than the previous check (!message.isGlobal) which would erroneously reject false.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, praising the use of a type-check. It doesn't provide a suggestion or point out a potential issue. It doesn't align with the rules for useful comments.

</details>


Workflow ID: <workflowid>`wflow_5ftffAvtdNsVmcdV`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can teach Ellipsis what to look for with :+1: / :-1: [feedback](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:07:04Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 6307d4c3e41488277a8fd4885cb514e82d9f7675 in <ellipsistime>1 minute and 8 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `38` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `5` drafted comments based on config settings.


<details>
<summary>1. <code>webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx:111</code></summary>

- **Draft comment:** 
Duplicate prop 'isGlobal' removed. Ensure only one instance of the prop is passed.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>2. <code>webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx:123</code></summary>

- **Draft comment:** 
Duplicate prop 'isGlobal' removed from the local rules configuration.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, stating that a duplicate property was removed. It doesn't provide a suggestion, ask for confirmation, or point out a potential issue. It doesn't align with the rules for useful comments.

</details>

<details>
<summary>3. <code>webview-ui/src/components/cline-rules/RulesToggleList.tsx:4</code></summary>

- **Draft comment:** 
Removed duplicate declaration of 'isGlobal' in prop destructuring. Clean up prop definitions.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, as it states what was done without providing any actionable feedback or suggestions. It doesn't ask for confirmation or suggest improvements, nor does it point out any potential issues with the change. Therefore, it should be removed according to the rules.

</details>

<details>
<summary>4. <code>webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx:111</code></summary>

- **Draft comment:** 
Good cleanup: The duplicate 'isGlobal' prop has been removed from the RulesToggleList instantiation. It’s important to pass this prop only once to avoid any potential ambiguity (both in the global and workspace rules sections).
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, explaining a change that was made without suggesting any action or asking for confirmation. It doesn't provide a suggestion or highlight a potential issue that needs addressing.

</details>

<details>
<summary>5. <code>webview-ui/src/components/cline-rules/RulesToggleList.tsx:4</code></summary>

- **Draft comment:** 
Removed the duplicate 'isGlobal' parameter in the component's destructuring. This avoids potential conflicts and clarifies which prop is being used.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, explaining a change that was made. It doesn't ask for clarification, suggest improvements, or point out potential issues. It simply states what was done and why, which violates the rule against purely informative comments.

</details>


Workflow ID: <workflowid>`wflow_Ho587iNS9C20tkU1`</workflowid>

</details>


----
You can teach Ellipsis what to look for with :+1: / :-1: [feedback](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:15:18Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on fe55c5afb28ae70aac02a6a13efb40fadad0b986 in <ellipsistime>34 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `21` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `4` drafted comments based on config settings.


<details>
<summary>1. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:60</code></summary>

- **Draft comment:** 
Good cleanup removing the stale comment in handleBlur.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:88</code></summary>

- **Draft comment:** 
Improved placeholder text increases clarity on allowed file extensions.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:58</code></summary>

- **Draft comment:** 
Removing the comment is fine, as the function behavior is self-explanatory.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>webview-ui/src/components/cline-rules/NewRuleRow.tsx:88</code></summary>

- **Draft comment:** 
The new placeholder text improves clarity by specifying acceptable extensions.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_Xr8t3CKY2XWaVzps`</workflowid>

</details>


----
You can teach Ellipsis what to look for with :+1: / :-1: [feedback](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:22:44Z
user: login: celestial-vault
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:23:12Z
user: login: celestial-vault
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:23:28Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on a9fe3c5a7eb26d38dd5bbdc175415e78676389d5 in <ellipsistime>50 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `13` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `2` drafted comments based on config settings.


<details>
<summary>1. <code>src/core/controller/index.ts:454</code></summary>

- **Draft comment:** 
Good update adding a stricter type check for 'message.filename'. Consider further sanitizing the filename (e.g., disallowing path separators) to avoid potential path traversal issues.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>2. <code>src/core/controller/index.ts:454</code></summary>

- **Draft comment:** 
Good improvement adding a type check for filename. Consider trimming the filename to catch whitespace-only strings.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_BiCa1uxKX9E2IZLF`</workflowid>

</details>


----
You can teach Ellipsis what to look for with :+1: / :-1: [feedback](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T19:38:52Z
user: login: canvrno
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T21:35:34Z
user: login: dcbartlett
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T23:21:40Z

Review comments:
diff_hunk: @@ -449,6 +449,36 @@ export class Controller {
 				break
 			case "openFile":
 				openFile(message.text!)
+				break
+			case "createRuleFile":
+				if (!message.isGlobal || !message.filename) {
path: src/core/controller/index.ts
user: login: ellipsis-dev[bot]
body: Potential bug in parameter validation: using `!message.isGlobal` incorrectly treats a valid false (local rule) as missing. Consider checking if `message.isGlobal` is undefined instead (e.g., `typeof message.isGlobal !== "boolean"`) to allow both true (global) and false (local).
```suggestion
				if (typeof message.isGlobal !== "boolean" || !message.filename) {
```
created_at: 2025-04-18T19:01:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -449,6 +449,36 @@ export class Controller {
 				break
 			case "openFile":
 				openFile(message.text!)
+				break
+			case "createRuleFile":
+				if (typeof message.isGlobal !== "boolean" || !message.filename) {
path: src/core/controller/index.ts
user: login: ellipsis-dev[bot]
body: Changing the check to `typeof message.isGlobal !== "boolean"` ensures that false (a valid boolean) isn’t rejected. Consider also explicitly verifying that `message.filename` is a non-empty string (e.g. using `typeof message.filename !== 'string' || !message.filename`) to tighten the input validation.
```suggestion
				if (typeof message.isGlobal !== "boolean" || typeof message.filename !== 'string' || !message.filename) {
```
created_at: 2025-04-18T19:07:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: webview-ui/src/components/cline-rules/NewRuleRow.tsx
user: login: celestial-vault
body: Used regular input instead of VSCode input because it wasn't giving me the control over the styling I needed.
created_at: 2025-04-18T19:23:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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
path: webview-ui/src/components/cline-rules/NewRuleRow.tsx
user: login: celestial-vault
body: Using VS Code theme variables however.
created_at: 2025-04-18T19:23:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-18T18:59:29Z
body: ###  🦋  Changeset detected

Latest commit: a9fe3c5a7eb26d38dd5bbdc175415e78676389d5

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name       | Type  |
| ---------- | ----- |
| claude-dev | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cline/cline/new/add-new-rule-row?filename=.changeset/sweet-wasps-happen.md&value=---%0A%22claude-dev%22%3A%20patch%0A---%0A%0AMOAR%20RULES%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-18T19:08:34Z
body: <!-- COVERAGE_REPORT -->
## Coverage Report

### Extension Coverage

Base branch: 45%

PR branch: 45%

✅ Coverage increased or remained the same

### Webview Coverage

Base branch: 11%

PR branch: 11%

⚠️ **Warning: Coverage decreased by 0.09%**

Consider adding tests to cover your changes.

### Overall Assessment

⚠️ **Test coverage has decreased in this PR**

Please consider adding tests to maintain or improve coverage.



<sub>Last updated: 2025-04-18T19:24:42.529665</sub>
author_association: CONTRIBUTOR
user: login: brianspierce
created_at: 2025-04-18T20:00:14Z
body: Checked rules creation, deletion, and editing functions. Confirmed that cline obeys rules I define in both a positive and negative sense (does what I ask, doesn't do what I ask it not to do). When rules in different files conflict, cline follows the oldest rule. When global rules conflict with workspace rules, cline follows the workspace rule.

LGTM


author_association: CONTRIBUTOR