Skip to content

Commit b461fdc

Browse files
feat: added UI for global rule creation (#7467)
feat: add rule scope dropdown for global rule creation - added global mode toggle - implemented conditional styling (white when enabled, gray when disabled) feat: improve global rules UI and fix debug mode issues - added dropdown button component to switch between global and workspace rule creation. - fixed global rules creation to use nested rules/ subdir in debug mode - prevent default glob patterns for global rules by returning undefined globs - generate unique rule names for global rules - added watcher for rules in global directory
1 parent a9f9ee2 commit b461fdc

File tree

8 files changed

+180
-17
lines changed

8 files changed

+180
-17
lines changed

core/config/markdown/loadMarkdownRules.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,18 @@ export async function loadMarkdownRules(ide: IDE): Promise<{
6464
// Process each markdown file
6565
for (const file of mdFiles) {
6666
try {
67-
const { relativePathOrBasename } = findUriInDirs(
67+
const { relativePathOrBasename, foundInDir } = findUriInDirs(
6868
file.path,
6969
await ide.getWorkspaceDirs(),
7070
);
71+
72+
// For global rules, use the full path
73+
// For workspace rules, use the relative path
74+
const fileUri = foundInDir ? relativePathOrBasename : file.path;
75+
7176
const rule = markdownToRule(file.content, {
7277
uriType: "file",
73-
fileUri: relativePathOrBasename,
78+
fileUri: fileUri,
7479
});
7580
rules.push({ ...rule, source: "rules-block", ruleFile: file.path });
7681
} catch (e) {

core/config/workspace/workspaceBlocks.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "@continuedev/config-yaml";
88
import * as YAML from "yaml";
99
import { IDE } from "../..";
10+
import { getContinueGlobalPath } from "../../util/paths";
1011
import { joinPathsToUri } from "../../util/uri";
1112

1213
const BLOCK_TYPE_CONFIG: Record<
@@ -114,8 +115,13 @@ export async function findAvailableFilename(
114115
blockType: BlockType,
115116
fileExists: (uri: string) => Promise<boolean>,
116117
extension?: string,
118+
isGlobal?: boolean,
117119
): Promise<string> {
118-
const baseFilename = `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
120+
// Differentiate filename based on whether its a global rule or a workspace rule
121+
const baseFilename =
122+
blockType === "rules" && isGlobal
123+
? "global-rule"
124+
: `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
119125
const fileExtension = extension ?? getFileExtension(blockType);
120126
let counter = 0;
121127
let fileUri: string;
@@ -156,3 +162,28 @@ export async function createNewWorkspaceBlockFile(
156162
await ide.writeFile(fileUri, fileContent);
157163
await ide.openFile(fileUri);
158164
}
165+
166+
export async function createNewGlobalRuleFile(ide: IDE): Promise<void> {
167+
try {
168+
const globalDir = getContinueGlobalPath();
169+
170+
// Create the rules subdirectory within the global directory
171+
const rulesDir = joinPathsToUri(globalDir, "rules");
172+
173+
const fileUri = await findAvailableFilename(
174+
rulesDir,
175+
"rules",
176+
ide.fileExists.bind(ide),
177+
undefined,
178+
true, // isGlobal = true for global rules
179+
);
180+
181+
const fileContent = getFileContent("rules");
182+
183+
await ide.writeFile(fileUri, fileContent);
184+
185+
await ide.openFile(fileUri);
186+
} catch (error) {
187+
throw error;
188+
}
189+
}

core/core.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ import {
6363
setupProviderConfig,
6464
setupQuickstartConfig,
6565
} from "./config/onboarding";
66-
import { createNewWorkspaceBlockFile } from "./config/workspace/workspaceBlocks";
66+
import {
67+
createNewGlobalRuleFile,
68+
createNewWorkspaceBlockFile,
69+
} from "./config/workspace/workspaceBlocks";
6770
import { MCPManagerSingleton } from "./context/mcp/MCPManagerSingleton";
6871
import { performAuth, removeMCPAuth } from "./context/mcp/MCPOauth";
6972
import { setMdmLicenseKey } from "./control-plane/mdm/mdm";
@@ -410,6 +413,17 @@ export class Core {
410413
);
411414
});
412415

416+
on("config/addGlobalRule", async (msg) => {
417+
try {
418+
await createNewGlobalRuleFile(this.ide);
419+
await this.configHandler.reloadConfig(
420+
"Global rule created (config/addGlobalRule message)",
421+
);
422+
} catch (error) {
423+
throw error;
424+
}
425+
});
426+
413427
on("config/openProfile", async (msg) => {
414428
await this.configHandler.openConfigProfile(
415429
msg.data.profileId,

core/protocol/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
9393
void,
9494
];
9595
"config/addLocalWorkspaceBlock": [{ blockType: BlockType }, void];
96+
"config/addGlobalRule": [undefined, void];
9697
"config/newPromptFile": [undefined, void];
9798
"config/newAssistantFile": [undefined, void];
9899
"config/ideSettingsUpdate": [IdeSettings, void];

core/protocol/passThrough.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
2222
"config/newAssistantFile",
2323
"config/ideSettingsUpdate",
2424
"config/addLocalWorkspaceBlock",
25+
"config/addGlobalRule",
2526
"config/getSerializedProfileInfo",
2627
"config/deleteModel",
2728
"config/refreshProfiles",

extensions/vscode/src/extension/VsCodeExtension.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from "fs";
2+
import path from "path";
23

34
import { IContextProvider } from "core";
45
import { ConfigHandler } from "core/config/ConfigHandler";
@@ -10,6 +11,7 @@ import {
1011
getConfigJsonPath,
1112
getConfigTsPath,
1213
getConfigYamlPath,
14+
getContinueGlobalPath,
1315
} from "core/util/paths";
1416
import { v4 as uuidv4 } from "uuid";
1517
import * as vscode from "vscode";
@@ -463,6 +465,18 @@ export class VsCodeExtension {
463465
void this.configHandler.reloadConfig("config.ts updated - fs file watch");
464466
});
465467

468+
// watch global rules directory for changes
469+
const globalRulesDir = path.join(getContinueGlobalPath(), "rules");
470+
if (fs.existsSync(globalRulesDir)) {
471+
fs.watch(globalRulesDir, { recursive: true }, (eventType, filename) => {
472+
if (filename && filename.endsWith(".md")) {
473+
void this.configHandler.reloadConfig(
474+
"Global rules directory updated - fs file watch",
475+
);
476+
}
477+
});
478+
}
479+
466480
vscode.workspace.onDidChangeTextDocument(async (event) => {
467481
if (event.contentChanges.length > 0) {
468482
selectionManager.documentChanged();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
2+
import { ToolTip } from "./gui/Tooltip";
3+
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "./ui";
4+
5+
interface DropdownOption {
6+
value: string;
7+
label: string;
8+
}
9+
10+
interface DropdownButtonProps {
11+
title: string;
12+
options: DropdownOption[];
13+
onOptionClick: (value: string) => void;
14+
addButtonTooltip?: string;
15+
className?: string;
16+
variant?: "default" | "sm";
17+
}
18+
19+
export function DropdownButton({
20+
title,
21+
options,
22+
onOptionClick,
23+
addButtonTooltip,
24+
className = "",
25+
variant = "default",
26+
}: DropdownButtonProps) {
27+
const isSmall = variant === "sm";
28+
const titleSize = isSmall ? "text-sm font-semibold" : "text-xl font-semibold";
29+
const marginBottom = isSmall ? "mb-2" : "mb-4";
30+
31+
return (
32+
<div
33+
className={`${marginBottom} flex items-center justify-between ${className}`}
34+
>
35+
<h3 className={`my-0 ${titleSize}`}>{title}</h3>
36+
<Listbox value={null} onChange={() => {}}>
37+
<div className="relative">
38+
<ToolTip content={addButtonTooltip}>
39+
<ListboxButton
40+
className={`ring-offset-background focus-visible:ring-ring border-description hover:enabled:bg-input hover:enabled:text-foreground text-description inline-flex h-7 items-center justify-center gap-1 whitespace-nowrap rounded-md border border-solid bg-transparent px-1.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50`}
41+
aria-label={addButtonTooltip}
42+
>
43+
<PlusIcon className="h-3 w-3" />
44+
<ChevronDownIcon className="h-3 w-3" />
45+
</ListboxButton>
46+
</ToolTip>
47+
<ListboxOptions className="min-w-32 max-w-36" anchor="bottom end">
48+
{options.map((option) => (
49+
<ListboxOption
50+
key={option.value}
51+
value={option.value}
52+
className={({ active }: { active: boolean }) =>
53+
`relative flex cursor-default select-none items-center gap-3 py-2 pl-4 pr-4 ${
54+
active
55+
? "bg-list-active text-list-active-foreground"
56+
: "text-foreground"
57+
}`
58+
}
59+
onClick={() => onOptionClick(option.value)}
60+
>
61+
<span className="block truncate">{option.label}</span>
62+
</ListboxOption>
63+
))}
64+
</ListboxOptions>
65+
</div>
66+
</Listbox>
67+
</div>
68+
);
69+
}

gui/src/pages/config/sections/RulesSection.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
DEFAULT_SYSTEM_MESSAGES_URL,
2020
} from "core/llm/defaultSystemMessages";
2121
import { getRuleDisplayName } from "core/llm/rules/rules-utils";
22-
import { useContext, useMemo } from "react";
22+
import { useContext, useMemo, useState } from "react";
23+
import { DropdownButton } from "../../../components/DropdownButton";
2324
import HeaderButtonWithToolTip from "../../../components/gui/HeaderButtonWithToolTip";
2425
import Switch from "../../../components/gui/Switch";
2526
import { useMainEditor } from "../../../components/mainInput/TipTapEditor";
@@ -182,8 +183,8 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule }) => {
182183
);
183184
}
184185

185-
const smallFont = useFontSize(-2);
186-
const tinyFont = useFontSize(-3);
186+
const smallFont = fontSize(-2);
187+
const tinyFont = fontSize(-3);
187188
return (
188189
<div
189190
className={`border-border flex flex-col rounded-sm px-2 py-1.5 transition-colors ${isDisabled ? "opacity-50" : ""}`}
@@ -407,18 +408,30 @@ function addDefaultSystemMessage(
407408
}
408409
}
409410

411+
// Define dropdown options for global rules
412+
const globalRulesOptions = [
413+
{ value: "workspace", label: "Current workspace" },
414+
{ value: "global", label: "Global" },
415+
];
416+
410417
function RulesSubSection() {
411418
const { selectedProfile } = useAuth();
412419
const config = useAppSelector((store) => store.config.config);
413420
const mode = useAppSelector((store) => store.session.mode);
414421
const ideMessenger = useContext(IdeMessengerContext);
415422
const isLocal = selectedProfile?.profileType === "local";
423+
const [globalRulesMode, setGlobalRulesMode] = useState<string>("workspace");
416424

417-
const handleAddRule = () => {
425+
const handleAddRule = (mode?: string) => {
426+
const currentMode = mode || globalRulesMode;
418427
if (isLocal) {
419-
void ideMessenger.request("config/addLocalWorkspaceBlock", {
420-
blockType: "rules",
421-
});
428+
if (currentMode === "global") {
429+
void ideMessenger.request("config/addGlobalRule", undefined);
430+
} else {
431+
void ideMessenger.request("config/addLocalWorkspaceBlock", {
432+
blockType: "rules",
433+
});
434+
}
422435
} else {
423436
void ideMessenger.request("controlPlane/openUrl", {
424437
path: "?type=rules",
@@ -427,6 +440,11 @@ function RulesSubSection() {
427440
}
428441
};
429442

443+
const handleOptionClick = (value: string) => {
444+
setGlobalRulesMode(value);
445+
handleAddRule(value);
446+
};
447+
430448
const sortedRules: RuleWithSource[] = useMemo(() => {
431449
const rules = [...config.rules.map((rule) => ({ ...rule }))];
432450

@@ -469,12 +487,22 @@ function RulesSubSection() {
469487

470488
return (
471489
<div>
472-
<ConfigHeader
473-
title="Rules"
474-
variant="sm"
475-
onAddClick={handleAddRule}
476-
addButtonTooltip="Add rule"
477-
/>
490+
{isLocal ? (
491+
<DropdownButton
492+
title="Rules"
493+
variant="sm"
494+
options={globalRulesOptions}
495+
onOptionClick={handleOptionClick}
496+
addButtonTooltip="Add rules"
497+
/>
498+
) : (
499+
<ConfigHeader
500+
title="Rules"
501+
variant="sm"
502+
onAddClick={() => handleAddRule()}
503+
addButtonTooltip="Add rules"
504+
/>
505+
)}
478506

479507
<Card>
480508
{sortedRules.length > 0 ? (

0 commit comments

Comments
 (0)