Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions core/config/markdown/loadMarkdownRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,18 @@ export async function loadMarkdownRules(ide: IDE): Promise<{
// Process each markdown file
for (const file of mdFiles) {
try {
const { relativePathOrBasename } = findUriInDirs(
const { relativePathOrBasename, foundInDir } = findUriInDirs(
file.path,
await ide.getWorkspaceDirs(),
);

// For global rules, use the full path
// For workspace rules, use the relative path
const fileUri = foundInDir ? relativePathOrBasename : file.path;

const rule = markdownToRule(file.content, {
uriType: "file",
fileUri: relativePathOrBasename,
fileUri: fileUri,
});
rules.push({ ...rule, source: "rules-block", ruleFile: file.path });
} catch (e) {
Expand Down
33 changes: 32 additions & 1 deletion core/config/workspace/workspaceBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@continuedev/config-yaml";
import * as YAML from "yaml";
import { IDE } from "../..";
import { getContinueGlobalPath } from "../../util/paths";
import { joinPathsToUri } from "../../util/uri";

const BLOCK_TYPE_CONFIG: Record<
Expand Down Expand Up @@ -101,8 +102,13 @@ export async function findAvailableFilename(
blockType: BlockType,
fileExists: (uri: string) => Promise<boolean>,
extension?: string,
isGlobal?: boolean,
): Promise<string> {
const baseFilename = `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
// Differentiate filename based on whether its a global rule or a workspace rule
const baseFilename =
blockType === "rules" && isGlobal
? "global-rule"
: `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
const fileExtension = extension ?? getFileExtension(blockType);
let counter = 0;
let fileUri: string;
Expand Down Expand Up @@ -143,3 +149,28 @@ export async function createNewWorkspaceBlockFile(
await ide.writeFile(fileUri, fileContent);
await ide.openFile(fileUri);
}

export async function createNewGlobalRuleFile(ide: IDE): Promise<void> {
try {
const globalDir = getContinueGlobalPath();

// Create the rules subdirectory within the global directory
const rulesDir = joinPathsToUri(globalDir, "rules");

const fileUri = await findAvailableFilename(
rulesDir,
"rules",
ide.fileExists.bind(ide),
undefined,
true, // isGlobal = true for global rules
);

const fileContent = getFileContent("rules");

await ide.writeFile(fileUri, fileContent);

await ide.openFile(fileUri);
} catch (error) {
throw error;
}
}
16 changes: 15 additions & 1 deletion core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ import {
setupProviderConfig,
setupQuickstartConfig,
} from "./config/onboarding";
import { createNewWorkspaceBlockFile } from "./config/workspace/workspaceBlocks";
import {
createNewGlobalRuleFile,
createNewWorkspaceBlockFile,
} from "./config/workspace/workspaceBlocks";
import { MCPManagerSingleton } from "./context/mcp/MCPManagerSingleton";
import { performAuth, removeMCPAuth } from "./context/mcp/MCPOauth";
import { setMdmLicenseKey } from "./control-plane/mdm/mdm";
Expand Down Expand Up @@ -419,6 +422,17 @@ export class Core {
);
});

on("config/addGlobalRule", async (msg) => {
try {
await createNewGlobalRuleFile(this.ide);
await this.configHandler.reloadConfig(
"Global rule created (config/addGlobalRule message)",
);
} catch (error) {
throw error;
}
});

on("config/openProfile", async (msg) => {
await this.configHandler.openConfigProfile(
msg.data.profileId,
Expand Down
1 change: 1 addition & 0 deletions core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
void,
];
"config/addLocalWorkspaceBlock": [{ blockType: BlockType }, void];
"config/addGlobalRule": [undefined, void];
"config/newPromptFile": [undefined, void];
"config/newAssistantFile": [undefined, void];
"config/ideSettingsUpdate": [IdeSettings, void];
Expand Down
1 change: 1 addition & 0 deletions core/protocol/passThrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
"config/newAssistantFile",
"config/ideSettingsUpdate",
"config/addLocalWorkspaceBlock",
"config/addGlobalRule",
"config/getSerializedProfileInfo",
"config/deleteModel",
"config/refreshProfiles",
Expand Down
14 changes: 14 additions & 0 deletions extensions/vscode/src/extension/VsCodeExtension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from "fs";
import path from "path";

import { IContextProvider } from "core";
import { ConfigHandler } from "core/config/ConfigHandler";
Expand All @@ -10,6 +11,7 @@ import {
getConfigJsonPath,
getConfigTsPath,
getConfigYamlPath,
getContinueGlobalPath,
} from "core/util/paths";
import { v4 as uuidv4 } from "uuid";
import * as vscode from "vscode";
Expand Down Expand Up @@ -463,6 +465,18 @@ export class VsCodeExtension {
void this.configHandler.reloadConfig("config.ts updated - fs file watch");
});

// watch global rules directory for changes
const globalRulesDir = path.join(getContinueGlobalPath(), "rules");
if (fs.existsSync(globalRulesDir)) {
fs.watch(globalRulesDir, { recursive: true }, (eventType, filename) => {
if (filename && filename.endsWith(".md")) {
void this.configHandler.reloadConfig(
"Global rules directory updated - fs file watch",
);
}
});
}

vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.contentChanges.length > 0) {
selectionManager.documentChanged();
Expand Down
69 changes: 69 additions & 0 deletions gui/src/components/DropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
import { ToolTip } from "./gui/Tooltip";
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "./ui";

interface DropdownOption {
value: string;
label: string;
}

interface DropdownButtonProps {
title: string;
options: DropdownOption[];
onOptionClick: (value: string) => void;
addButtonTooltip?: string;
className?: string;
variant?: "default" | "sm";
}

export function DropdownButton({
title,
options,
onOptionClick,
addButtonTooltip,
className = "",
variant = "default",
}: DropdownButtonProps) {
const isSmall = variant === "sm";
const titleSize = isSmall ? "text-sm font-semibold" : "text-xl font-semibold";
const marginBottom = isSmall ? "mb-2" : "mb-4";

return (
<div
className={`${marginBottom} flex items-center justify-between ${className}`}
>
<h3 className={`my-0 ${titleSize}`}>{title}</h3>
<Listbox value={null} onChange={() => {}}>
<div className="relative">
<ToolTip content={addButtonTooltip}>
<ListboxButton
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`}
aria-label={addButtonTooltip}
>
<PlusIcon className="h-3 w-3" />
<ChevronDownIcon className="h-3 w-3" />
</ListboxButton>
</ToolTip>
<ListboxOptions className="min-w-32 max-w-36" anchor="bottom end">
{options.map((option) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ active }: { active: boolean }) =>
`relative flex cursor-default select-none items-center gap-3 py-2 pl-4 pr-4 ${
active
? "bg-list-active text-list-active-foreground"
: "text-foreground"
}`
}
onClick={() => onOptionClick(option.value)}
>
<span className="block truncate">{option.label}</span>
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
</div>
);
}
54 changes: 41 additions & 13 deletions gui/src/pages/config/sections/RulesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
DEFAULT_SYSTEM_MESSAGES_URL,
} from "core/llm/defaultSystemMessages";
import { getRuleDisplayName } from "core/llm/rules/rules-utils";
import { useContext, useMemo } from "react";
import { useContext, useMemo, useState } from "react";
import { DropdownButton } from "../../../components/DropdownButton";
import HeaderButtonWithToolTip from "../../../components/gui/HeaderButtonWithToolTip";
import Switch from "../../../components/gui/Switch";
import { useMainEditor } from "../../../components/mainInput/TipTapEditor";
Expand Down Expand Up @@ -182,8 +183,8 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule }) => {
);
}

const smallFont = useFontSize(-2);
const tinyFont = useFontSize(-3);
const smallFont = fontSize(-2);
const tinyFont = fontSize(-3);
return (
<div
className={`border-border flex flex-col rounded-sm px-2 py-1.5 transition-colors ${isDisabled ? "opacity-50" : ""}`}
Expand Down Expand Up @@ -407,18 +408,30 @@ function addDefaultSystemMessage(
}
}

// Define dropdown options for global rules
const globalRulesOptions = [
{ value: "workspace", label: "Current workspace" },
{ value: "global", label: "Global" },
];

function RulesSubSection() {
const { selectedProfile } = useAuth();
const config = useAppSelector((store) => store.config.config);
const mode = useAppSelector((store) => store.session.mode);
const ideMessenger = useContext(IdeMessengerContext);
const isLocal = selectedProfile?.profileType === "local";
const [globalRulesMode, setGlobalRulesMode] = useState<string>("workspace");

const handleAddRule = () => {
const handleAddRule = (mode?: string) => {
const currentMode = mode || globalRulesMode;
if (isLocal) {
void ideMessenger.request("config/addLocalWorkspaceBlock", {
blockType: "rules",
});
if (currentMode === "global") {
void ideMessenger.request("config/addGlobalRule", undefined);
} else {
void ideMessenger.request("config/addLocalWorkspaceBlock", {
blockType: "rules",
});
}
} else {
void ideMessenger.request("controlPlane/openUrl", {
path: "?type=rules",
Expand All @@ -427,6 +440,11 @@ function RulesSubSection() {
}
};

const handleOptionClick = (value: string) => {
setGlobalRulesMode(value);
handleAddRule(value);
};

const sortedRules: RuleWithSource[] = useMemo(() => {
const rules = [...config.rules.map((rule) => ({ ...rule }))];

Expand Down Expand Up @@ -469,12 +487,22 @@ function RulesSubSection() {

return (
<div>
<ConfigHeader
title="Rules"
variant="sm"
onAddClick={handleAddRule}
addButtonTooltip="Add rule"
/>
{isLocal ? (
<DropdownButton
title="Rules"
variant="sm"
options={globalRulesOptions}
onOptionClick={handleOptionClick}
addButtonTooltip="Add rules"
/>
) : (
<ConfigHeader
title="Rules"
variant="sm"
onAddClick={() => handleAddRule()}
addButtonTooltip="Add rules"
/>
)}

<Card>
{sortedRules.length > 0 ? (
Expand Down
Loading