Skip to content

Commit 8101b08

Browse files
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 0910caf commit 8101b08

File tree

8 files changed

+181
-19
lines changed

8 files changed

+181
-19
lines changed

core/config/markdown/loadMarkdownRules.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ export async function loadMarkdownRules(ide: IDE): Promise<{
3030
// Process each markdown file
3131
for (const file of mdFiles) {
3232
try {
33-
const { relativePathOrBasename } = findUriInDirs(
33+
const { relativePathOrBasename, foundInDir } = findUriInDirs(
3434
file.path,
3535
await ide.getWorkspaceDirs(),
3636
);
37+
38+
// For global rules, use the full path
39+
// For workspace rules, use the relative path
40+
const fileUri = foundInDir ? relativePathOrBasename : file.path;
41+
3742
const rule = markdownToRule(file.content, {
3843
uriType: "file",
39-
fileUri: relativePathOrBasename,
44+
fileUri: fileUri,
4045
});
4146
rules.push({ ...rule, source: "rules-block", ruleFile: file.path });
4247
} catch (e) {

core/config/workspace/workspaceBlocks.ts

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

1112
const BLOCK_TYPE_CONFIG: Record<
@@ -101,8 +102,13 @@ export async function findAvailableFilename(
101102
blockType: BlockType,
102103
fileExists: (uri: string) => Promise<boolean>,
103104
extension?: string,
105+
isGlobal?: boolean,
104106
): Promise<string> {
105-
const baseFilename = `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
107+
// Differentiate filename based on whether its a global rule or a workspace rule
108+
const baseFilename =
109+
blockType === "rules" && isGlobal
110+
? "global-rule"
111+
: `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
106112
const fileExtension = extension ?? getFileExtension(blockType);
107113
let counter = 0;
108114
let fileUri: string;
@@ -143,3 +149,28 @@ export async function createNewWorkspaceBlockFile(
143149
await ide.writeFile(fileUri, fileContent);
144150
await ide.openFile(fileUri);
145151
}
152+
153+
export async function createNewGlobalRuleFile(ide: IDE): Promise<void> {
154+
try {
155+
const globalDir = getContinueGlobalPath();
156+
157+
// Create the rules subdirectory within the global directory
158+
const rulesDir = joinPathsToUri(globalDir, "rules");
159+
160+
const fileUri = await findAvailableFilename(
161+
rulesDir,
162+
"rules",
163+
ide.fileExists.bind(ide),
164+
undefined,
165+
true, // isGlobal = true for global rules
166+
);
167+
168+
const fileContent = getFileContent("rules");
169+
170+
await ide.writeFile(fileUri, fileContent);
171+
172+
await ide.openFile(fileUri);
173+
} catch (error) {
174+
throw error;
175+
}
176+
}

core/core.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
setupProviderConfig,
6060
setupQuickstartConfig,
6161
} from "./config/onboarding";
62-
import { createNewWorkspaceBlockFile } from "./config/workspace/workspaceBlocks";
62+
import {
63+
createNewGlobalRuleFile,
64+
createNewWorkspaceBlockFile,
65+
} from "./config/workspace/workspaceBlocks";
6366
import { MCPManagerSingleton } from "./context/mcp/MCPManagerSingleton";
6467
import { performAuth, removeMCPAuth } from "./context/mcp/MCPOauth";
6568
import { setMdmLicenseKey } from "./control-plane/mdm/mdm";
@@ -374,6 +377,17 @@ export class Core {
374377
);
375378
});
376379

380+
on("config/addGlobalRule", async (msg) => {
381+
try {
382+
await createNewGlobalRuleFile(this.ide);
383+
await this.configHandler.reloadConfig(
384+
"Global rule created (config/addGlobalRule message)",
385+
);
386+
} catch (error) {
387+
throw error;
388+
}
389+
});
390+
377391
on("config/openProfile", async (msg) => {
378392
await this.configHandler.openConfigProfile(
379393
msg.data.profileId,

core/protocol/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
8686
void,
8787
];
8888
"config/addLocalWorkspaceBlock": [{ blockType: BlockType }, void];
89+
"config/addGlobalRule": [undefined, void];
8990
"config/newPromptFile": [undefined, void];
9091
"config/newAssistantFile": [undefined, void];
9192
"config/ideSettingsUpdate": [IdeSettings, void];

core/protocol/passThrough.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
2121
"config/newAssistantFile",
2222
"config/ideSettingsUpdate",
2323
"config/addLocalWorkspaceBlock",
24+
"config/addGlobalRule",
2425
"config/getSerializedProfileInfo",
2526
"config/deleteModel",
2627
"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: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
BookmarkIcon as BookmarkOutline,
55
EyeIcon,
66
PencilIcon,
7-
PlusIcon,
87
} from "@heroicons/react/24/outline";
98
import { BookmarkIcon as BookmarkSolid } from "@heroicons/react/24/solid";
109
import {
@@ -19,11 +18,12 @@ import {
1918
DEFAULT_PLAN_SYSTEM_MESSAGE,
2019
DEFAULT_SYSTEM_MESSAGES_URL,
2120
} from "core/llm/defaultSystemMessages";
22-
import { useContext, useMemo } from "react";
21+
import { useContext, useMemo, useState } from "react";
22+
import { DropdownButton } from "../../../components/DropdownButton";
2323
import HeaderButtonWithToolTip from "../../../components/gui/HeaderButtonWithToolTip";
2424
import Switch from "../../../components/gui/Switch";
2525
import { useMainEditor } from "../../../components/mainInput/TipTapEditor";
26-
import { Button, Card, EmptyState, useFontSize } from "../../../components/ui";
26+
import { Card, EmptyState } from "../../../components/ui";
2727
import { useAuth } from "../../../context/Auth";
2828
import { IdeMessengerContext } from "../../../context/IdeMessenger";
2929
import { useBookmarkedSlashCommands } from "../../../hooks/useBookmarkedSlashCommands";
@@ -202,8 +202,8 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule }) => {
202202
);
203203
}
204204

205-
const smallFont = useFontSize(-2);
206-
const tinyFont = useFontSize(-3);
205+
const smallFont = fontSize(-2);
206+
const tinyFont = fontSize(-3);
207207
return (
208208
<div
209209
className={`border-border flex flex-col rounded-sm px-2 py-1.5 transition-colors ${isDisabled ? "opacity-50" : ""}`}
@@ -427,18 +427,30 @@ function addDefaultSystemMessage(
427427
}
428428
}
429429

430+
// Define dropdown options for global rules
431+
const globalRulesOptions = [
432+
{ value: "workspace", label: "Current workspace" },
433+
{ value: "global", label: "Global" },
434+
];
435+
430436
function RulesSubSection() {
431437
const { selectedProfile } = useAuth();
432438
const config = useAppSelector((store) => store.config.config);
433439
const mode = useAppSelector((store) => store.session.mode);
434440
const ideMessenger = useContext(IdeMessengerContext);
435441
const isLocal = selectedProfile?.profileType === "local";
442+
const [globalRulesMode, setGlobalRulesMode] = useState<string>("workspace");
436443

437-
const handleAddRule = () => {
444+
const handleAddRule = (mode?: string) => {
445+
const currentMode = mode || globalRulesMode;
438446
if (isLocal) {
439-
void ideMessenger.request("config/addLocalWorkspaceBlock", {
440-
blockType: "rules",
441-
});
447+
if (currentMode === "global") {
448+
void ideMessenger.request("config/addGlobalRule", undefined);
449+
} else {
450+
void ideMessenger.request("config/addLocalWorkspaceBlock", {
451+
blockType: "rules",
452+
});
453+
}
442454
} else {
443455
void ideMessenger.request("controlPlane/openUrl", {
444456
path: "?type=rules",
@@ -447,6 +459,11 @@ function RulesSubSection() {
447459
}
448460
};
449461

462+
const handleOptionClick = (value: string) => {
463+
setGlobalRulesMode(value);
464+
handleAddRule(value);
465+
};
466+
450467
const sortedRules: RuleWithSource[] = useMemo(() => {
451468
const rules = [...config.rules.map((rule) => ({ ...rule }))];
452469

@@ -489,12 +506,22 @@ function RulesSubSection() {
489506

490507
return (
491508
<div>
492-
<ConfigHeader
493-
title="Rules"
494-
variant="sm"
495-
onAddClick={handleAddRule}
496-
addButtonTooltip="Add rule"
497-
/>
509+
{isLocal ? (
510+
<DropdownButton
511+
title="Rules"
512+
variant="sm"
513+
options={globalRulesOptions}
514+
onOptionClick={handleOptionClick}
515+
addButtonTooltip="Add rules"
516+
/>
517+
) : (
518+
<ConfigHeader
519+
title="Rules"
520+
variant="sm"
521+
onAddClick={() => handleAddRule()}
522+
addButtonTooltip="Add rules"
523+
/>
524+
)}
498525

499526
<Card>
500527
{sortedRules.length > 0 ? (

0 commit comments

Comments
 (0)