Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
39 changes: 33 additions & 6 deletions core/config/workspace/workspaceBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ConfigYaml,
createPromptMarkdown,
createRuleMarkdown,
sanitizeRuleName,
} from "@continuedev/config-yaml";
import * as YAML from "yaml";
import { IDE } from "../..";
Expand Down Expand Up @@ -116,13 +117,31 @@ export async function findAvailableFilename(
fileExists: (uri: string) => Promise<boolean>,
extension?: string,
isGlobal?: boolean,
baseFilenameOverride?: string,
): Promise<string> {
// 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 baseFilename: string;

const trimmedOverride = baseFilenameOverride?.trim();
if (trimmedOverride) {
if (blockType === "rules") {
const withoutExtension = trimmedOverride.replace(/\.[^./\\]+$/, "");
const sanitized = sanitizeRuleName(withoutExtension);
baseFilename =
sanitized ||
(isGlobal
? "global-rule"
: `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`);
} else {
baseFilename = trimmedOverride;
}
} else {
baseFilename =
blockType === "rules" && isGlobal
? "global-rule"
: `new-${BLOCK_TYPE_CONFIG[blockType]?.filename}`;
}

let counter = 0;
let fileUri: string;

Expand All @@ -141,6 +160,7 @@ export async function findAvailableFilename(
export async function createNewWorkspaceBlockFile(
ide: IDE,
blockType: BlockType,
baseFilename?: string,
): Promise<void> {
const workspaceDirs = await ide.getWorkspaceDirs();
if (workspaceDirs.length === 0) {
Expand All @@ -155,6 +175,9 @@ export async function createNewWorkspaceBlockFile(
baseDirUri,
blockType,
ide.fileExists.bind(ide),
undefined,
false,
baseFilename,
);

const fileContent = getFileContent(blockType);
Expand All @@ -163,7 +186,10 @@ export async function createNewWorkspaceBlockFile(
await ide.openFile(fileUri);
}

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

Expand All @@ -176,6 +202,7 @@ export async function createNewGlobalRuleFile(ide: IDE): Promise<void> {
ide.fileExists.bind(ide),
undefined,
true, // isGlobal = true for global rules
baseFilename,
);

const fileContent = getFileContent("rules");
Expand Down
8 changes: 6 additions & 2 deletions core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,15 +409,19 @@ export class Core {
});

on("config/addLocalWorkspaceBlock", async (msg) => {
await createNewWorkspaceBlockFile(this.ide, msg.data.blockType);
await createNewWorkspaceBlockFile(
this.ide,
msg.data.blockType,
msg.data.baseFilename,
);
await this.configHandler.reloadConfig(
"Local block created (config/addLocalWorkspaceBlock message)",
);
});

on("config/addGlobalRule", async (msg) => {
try {
await createNewGlobalRuleFile(this.ide);
await createNewGlobalRuleFile(this.ide, msg.data?.baseFilename);
await this.configHandler.reloadConfig(
"Global rule created (config/addGlobalRule message)",
);
Expand Down
7 changes: 5 additions & 2 deletions core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ export type ToCoreFromIdeOrWebviewProtocol = {
},
void,
];
"config/addLocalWorkspaceBlock": [{ blockType: BlockType }, void];
"config/addGlobalRule": [undefined, void];
"config/addLocalWorkspaceBlock": [
{ blockType: BlockType; baseFilename?: string },
void,
];
"config/addGlobalRule": [undefined | { baseFilename?: string }, void];
"config/newPromptFile": [undefined, void];
"config/newAssistantFile": [undefined, void];
"config/ideSettingsUpdate": [IdeSettings, void];
Expand Down
100 changes: 100 additions & 0 deletions gui/src/components/dialogs/AddRuleDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useContext, useLayoutEffect, useRef, useState } from "react";
import { useDispatch } from "react-redux";
import { Input, SecondaryButton } from "..";
import { IdeMessengerContext } from "../../context/IdeMessenger";
import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";

function AddRuleDialog({ mode }: { mode: "workspace" | "global" }) {
const dispatch = useDispatch();
const ideMessenger = useContext(IdeMessengerContext);
const [name, setName] = useState("");
const [error, setError] = useState<string | undefined>();
const [isSubmitting, setIsSubmitting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

useLayoutEffect(() => {
// focus on input after a short delay
const timer = setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 100);
return () => clearTimeout(timer);
}, []);

const closeDialog = () => {
dispatch(setShowDialog(false));
dispatch(setDialogMessage(undefined));
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setError("File name is required");
return;
}
setError(undefined);
setIsSubmitting(true);
try {
if (mode === "global") {
ideMessenger.post("config/addGlobalRule", {
baseFilename: trimmed,
});
} else {
ideMessenger.post("config/addLocalWorkspaceBlock", {
blockType: "rules",
baseFilename: trimmed,
});
}
closeDialog();
} catch (err) {
setIsSubmitting(false);
setError("Failed to create rule file");
}
};

const title = mode === "global" ? "Add global rule file" : "Add rule file";

return (
<div className="px-2 pt-4 sm:px-4">
<div>
<h1 className="mb-0">{title}</h1>
<p className="m-0 mt-2 p-0 text-stone-500">
Choose a name for the new rule file.
</p>
<form onSubmit={handleSubmit} className="mt-3 flex flex-col gap-2">
<label className="flex w-full flex-col gap-1">
<span>File name</span>
<Input
ref={inputRef}
type="text"
placeholder="ex: api-guidelines"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
{error && <p className="text-xs text-red-500">{error}</p>}
<div className="mt-2 flex flex-row justify-end gap-2">
<SecondaryButton
className="min-w-16"
disabled={!name.trim() || isSubmitting}
type="submit"
>
Create
</SecondaryButton>
<SecondaryButton
type="button"
className="min-w-16"
onClick={closeDialog}
>
Cancel
</SecondaryButton>
</div>
</form>
</div>
</div>
);
}

export default AddRuleDialog;
17 changes: 10 additions & 7 deletions gui/src/pages/config/sections/RulesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { getRuleDisplayName } from "core/llm/rules/rules-utils";
import { useContext, useMemo, useState } from "react";
import { DropdownButton } from "../../../components/DropdownButton";
import AddRuleDialog from "../../../components/dialogs/AddRuleDialog";
import HeaderButtonWithToolTip from "../../../components/gui/HeaderButtonWithToolTip";
import Switch from "../../../components/gui/Switch";
import {
Expand Down Expand Up @@ -393,19 +394,21 @@ function RulesSubSection() {
const config = useAppSelector((store) => store.config.config);
const mode = useAppSelector((store) => store.session.mode);
const ideMessenger = useContext(IdeMessengerContext);
const dispatch = useAppDispatch();
const isLocal = selectedProfile?.profileType === "local";
const [globalRulesMode, setGlobalRulesMode] = useState<string>("workspace");

const handleAddRule = (mode?: string) => {
const currentMode = mode || globalRulesMode;
if (isLocal) {
if (currentMode === "global") {
void ideMessenger.request("config/addGlobalRule", undefined);
} else {
void ideMessenger.request("config/addLocalWorkspaceBlock", {
blockType: "rules",
});
}
dispatch(setShowDialog(true));
dispatch(
setDialogMessage(
<AddRuleDialog
mode={currentMode === "global" ? "global" : "workspace"}
/>,
),
);
} else {
void ideMessenger.request("controlPlane/openUrl", {
path: "?type=rules",
Expand Down
Loading