Skip to content

Commit 274349f

Browse files
authored
add ui component for external rules files (RooCodeInc#3291)
* base * svg * delete refresh * changeset
1 parent 6d24e22 commit 274349f

File tree

9 files changed

+164
-8
lines changed

9 files changed

+164
-8
lines changed

.changeset/two-jobs-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
add ui for windsurf and cursor rules

src/core/controller/file/deleteRuleFile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
deleteRuleFile as deleteRuleFileImpl,
66
refreshClineRulesToggles,
77
} from "@core/context/instructions/user-instructions/cline-rules"
8+
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
89
import * as vscode from "vscode"
910
import * as path from "path"
1011
import { cwd } from "@core/task"
@@ -32,6 +33,7 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
3233
}
3334

3435
await refreshClineRulesToggles(controller.context, cwd)
36+
await refreshExternalRulesToggles(controller.context, cwd)
3537
await controller.postStateToWebview()
3638

3739
const fileName = path.basename(request.rulePath)

src/core/controller/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import {
4848
} from "../storage/state"
4949
import { Task, cwd } from "../task"
5050
import { ClineRulesToggles } from "@shared/cline-rules"
51-
import { createRuleFile, refreshClineRulesToggles } from "../context/instructions/user-instructions/cline-rules"
51+
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
52+
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
5253

5354
/*
5455
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -375,6 +376,7 @@ export class Controller {
375376
break
376377
case "refreshClineRules":
377378
await refreshClineRulesToggles(this.context, cwd)
379+
await refreshExternalRulesToggles(this.context, cwd)
378380
await this.postStateToWebview()
379381
break
380382
case "openInBrowser":
@@ -516,6 +518,32 @@ export class Controller {
516518
}
517519
break
518520
}
521+
case "toggleWindsurfRule": {
522+
const { rulePath, enabled } = message
523+
if (rulePath && typeof enabled === "boolean") {
524+
const toggles =
525+
((await getWorkspaceState(this.context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
526+
toggles[rulePath] = enabled
527+
await updateWorkspaceState(this.context, "localWindsurfRulesToggles", toggles)
528+
await this.postStateToWebview()
529+
} else {
530+
console.error("toggleWindsurfRule: Missing or invalid parameters")
531+
}
532+
break
533+
}
534+
case "toggleCursorRule": {
535+
const { rulePath, enabled } = message
536+
if (rulePath && typeof enabled === "boolean") {
537+
const toggles =
538+
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
539+
toggles[rulePath] = enabled
540+
await updateWorkspaceState(this.context, "localCursorRulesToggles", toggles)
541+
await this.postStateToWebview()
542+
} else {
543+
console.error("toggleCursorRule: Missing or invalid parameters")
544+
}
545+
break
546+
}
519547
case "requestTotalTasksSize": {
520548
this.refreshTotalTasksSize()
521549
break
@@ -1765,6 +1793,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
17651793
const localClineRulesToggles =
17661794
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
17671795

1796+
const localWindsurfRulesToggles =
1797+
((await getWorkspaceState(this.context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
1798+
1799+
const localCursorRulesToggles =
1800+
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
1801+
17681802
return {
17691803
version: this.context.extension?.packageJSON?.version ?? "",
17701804
apiConfiguration,
@@ -1789,6 +1823,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
17891823
vscMachineId: vscode.env.machineId,
17901824
globalClineRulesToggles: globalClineRulesToggles || {},
17911825
localClineRulesToggles: localClineRulesToggles || {},
1826+
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
1827+
localCursorRulesToggles: localCursorRulesToggles || {},
17921828
shellIntegrationTimeout,
17931829
}
17941830
}

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export interface ExtensionState {
142142
vscMachineId: string
143143
globalClineRulesToggles: ClineRulesToggles
144144
localClineRulesToggles: ClineRulesToggles
145+
localCursorRulesToggles: ClineRulesToggles
146+
localWindsurfRulesToggles: ClineRulesToggles
145147
}
146148

147149
export interface ClineMessage {

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export interface WebviewMessage {
6767
| "toggleFavoriteModel"
6868
| "grpc_request"
6969
| "toggleClineRule"
70+
| "toggleCursorRule"
71+
| "toggleWindsurfRule"
7072
| "deleteClineRule"
7173
| "copyToClipboard"
7274
| "updateTerminalConnectionTimeout"

webview-ui/src/components/cline-rules/ClineRulesToggleModal.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import RulesToggleList from "./RulesToggleList"
88
import Tooltip from "@/components/common/Tooltip"
99

1010
const ClineRulesToggleModal: React.FC = () => {
11-
const { globalClineRulesToggles = {}, localClineRulesToggles = {} } = useExtensionState()
11+
const {
12+
globalClineRulesToggles = {},
13+
localClineRulesToggles = {},
14+
localCursorRulesToggles = {},
15+
localWindsurfRulesToggles = {},
16+
} = useExtensionState()
1217
const [isVisible, setIsVisible] = useState(false)
1318
const buttonRef = useRef<HTMLDivElement>(null)
1419
const modalRef = useRef<HTMLDivElement>(null)
@@ -32,6 +37,14 @@ const ClineRulesToggleModal: React.FC = () => {
3237
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
3338
.sort(([a], [b]) => a.localeCompare(b))
3439

40+
const cursorRules = Object.entries(localCursorRulesToggles || {})
41+
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
42+
.sort(([a], [b]) => a.localeCompare(b))
43+
44+
const windsurfRules = Object.entries(localWindsurfRulesToggles || {})
45+
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
46+
.sort(([a], [b]) => a.localeCompare(b))
47+
3548
// Handle toggle rule
3649
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
3750
vscode.postMessage({
@@ -42,6 +55,22 @@ const ClineRulesToggleModal: React.FC = () => {
4255
})
4356
}
4457

58+
const toggleCursorRule = (rulePath: string, enabled: boolean) => {
59+
vscode.postMessage({
60+
type: "toggleCursorRule",
61+
rulePath,
62+
enabled,
63+
})
64+
}
65+
66+
const toggleWindsurfRule = (rulePath: string, enabled: boolean) => {
67+
vscode.postMessage({
68+
type: "toggleWindsurfRule",
69+
rulePath,
70+
enabled,
71+
})
72+
}
73+
4574
// Close modal when clicking outside
4675
useClickAway(modalRef, () => {
4776
setIsVisible(false)
@@ -117,6 +146,9 @@ const ClineRulesToggleModal: React.FC = () => {
117146
toggleRule={(rulePath, enabled) => toggleRule(true, rulePath, enabled)}
118147
listGap="small"
119148
isGlobal={true}
149+
ruleType={"cline"}
150+
showNewRule={true}
151+
showNoRules={true}
120152
/>
121153
</div>
122154

@@ -128,6 +160,27 @@ const ClineRulesToggleModal: React.FC = () => {
128160
toggleRule={(rulePath, enabled) => toggleRule(false, rulePath, enabled)}
129161
listGap="small"
130162
isGlobal={false}
163+
ruleType={"cline"}
164+
showNewRule={false}
165+
showNoRules={false}
166+
/>
167+
<RulesToggleList
168+
rules={cursorRules}
169+
toggleRule={toggleCursorRule}
170+
listGap="small"
171+
isGlobal={false}
172+
ruleType={"cursor"}
173+
showNewRule={false}
174+
showNoRules={false}
175+
/>
176+
<RulesToggleList
177+
rules={windsurfRules}
178+
toggleRule={toggleWindsurfRule}
179+
listGap="small"
180+
isGlobal={false}
181+
ruleType={"windsurf"}
182+
showNewRule={true}
183+
showNoRules={localRules.length === 0 && cursorRules.length === 0 && windsurfRules.length === 0}
131184
/>
132185
</div>
133186
</div>

webview-ui/src/components/cline-rules/RuleRow.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,53 @@ const RuleRow: React.FC<{
66
rulePath: string
77
enabled: boolean
88
isGlobal: boolean
9+
ruleType: string
910
toggleRule: (rulePath: string, enabled: boolean) => void
10-
}> = ({ rulePath, enabled, isGlobal, toggleRule }) => {
11+
}> = ({ rulePath, enabled, isGlobal, toggleRule, ruleType }) => {
1112
// Get the filename from the path for display
1213
const displayName = rulePath.split("/").pop() || rulePath
1314

15+
const getRuleTypeIcon = () => {
16+
switch (ruleType) {
17+
case "cursor":
18+
return (
19+
<svg
20+
xmlns="http://www.w3.org/2000/svg"
21+
width="16"
22+
height="16"
23+
viewBox="0 0 24 24"
24+
style={{ verticalAlign: "middle" }}>
25+
<g fill="none" stroke="currentColor" strokeWidth="1.2">
26+
<path d="M12 4L5 8l7 4 7-4-7-4z" fill="rgba(255,255,255,0.2)" />
27+
<path d="M5 8v8l7 4v-8L5 8z" fill="rgba(255,255,255,0.1)" />
28+
<path d="M19 8v8l-7 4v-8l7-4z" fill="rgba(255,255,255,0.15)" />
29+
<line x1="5" y1="8" x2="12" y2="12" />
30+
<line x1="12" y1="12" x2="19" y2="8" />
31+
<line x1="12" y1="12" x2="12" y2="20" />
32+
</g>
33+
</svg>
34+
)
35+
case "windsurf":
36+
return (
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
width="16"
40+
height="16"
41+
viewBox="0 0 24 24"
42+
style={{ verticalAlign: "middle" }}>
43+
<g fill="currentColor" stroke="currentColor" strokeWidth="1">
44+
<path d="M6 18L16 5L14 18H6z" fill="currentColor" />
45+
<line x1="14" y1="18" x2="16" y2="5" strokeWidth="1.5" />
46+
<path d="M4 19h12c0.5 0 1-0.3 1-1s-0.3-1-1-1H4c-0.5 0-1 0.3-1 1s0.3 1 1 1z" fill="currentColor" />
47+
<line x1="14" y1="13" x2="16" y2="9" strokeWidth="1" />
48+
</g>
49+
</svg>
50+
)
51+
default:
52+
return null
53+
}
54+
}
55+
1456
const handleEditClick = () => {
1557
FileServiceClient.openFile({ value: rulePath }).catch((err) => console.error("Failed to open file:", err))
1658
}
@@ -31,6 +73,7 @@ const RuleRow: React.FC<{
3173
enabled ? "opacity-100" : "opacity-60"
3274
}`}>
3375
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
76+
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
3477
{displayName}
3578
</span>
3679

webview-ui/src/components/cline-rules/RulesToggleList.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ const RulesToggleList = ({
66
toggleRule,
77
listGap = "medium",
88
isGlobal,
9+
ruleType,
10+
showNewRule,
11+
showNoRules,
912
}: {
1013
rules: [string, boolean][]
1114
toggleRule: (rulePath: string, enabled: boolean) => void
1215
listGap?: "small" | "medium" | "large"
1316
isGlobal: boolean
17+
ruleType: string
18+
showNewRule: boolean
19+
showNoRules: boolean
1420
}) => {
1521
const gapClasses = {
1622
small: "gap-0",
@@ -31,16 +37,19 @@ const RulesToggleList = ({
3137
enabled={enabled}
3238
isGlobal={isGlobal}
3339
toggleRule={toggleRule}
40+
ruleType={ruleType}
3441
/>
3542
))}
36-
<NewRuleRow isGlobal={isGlobal} />
43+
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
3744
</>
3845
) : (
3946
<>
40-
<div className="flex flex-col items-center gap-3 my-3 text-[var(--vscode-descriptionForeground)]">
41-
No rules found
42-
</div>
43-
<NewRuleRow isGlobal={isGlobal} />
47+
{showNoRules && (
48+
<div className="flex flex-col items-center gap-3 my-3 text-[var(--vscode-descriptionForeground)]">
49+
No rules found
50+
</div>
51+
)}
52+
{showNewRule && <NewRuleRow isGlobal={isGlobal} />}
4453
</>
4554
)}
4655
</div>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export const ExtensionStateContextProvider: React.FC<{
7070
planActSeparateModelsSetting: true,
7171
globalClineRulesToggles: {},
7272
localClineRulesToggles: {},
73+
localCursorRulesToggles: {},
74+
localWindsurfRulesToggles: {},
7375
shellIntegrationTimeout: 4000, // default timeout for shell integration
7476
})
7577
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -219,6 +221,8 @@ export const ExtensionStateContextProvider: React.FC<{
219221
mcpTab,
220222
globalClineRulesToggles: state.globalClineRulesToggles || {},
221223
localClineRulesToggles: state.localClineRulesToggles || {},
224+
localCursorRulesToggles: state.localCursorRulesToggles || {},
225+
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
222226
setApiConfiguration: (value) =>
223227
setState((prevState) => ({
224228
...prevState,

0 commit comments

Comments
 (0)