diff --git a/core/config/workspace/workspaceBlocks.ts b/core/config/workspace/workspaceBlocks.ts index 7dcdecf634f..ace83605109 100644 --- a/core/config/workspace/workspaceBlocks.ts +++ b/core/config/workspace/workspaceBlocks.ts @@ -3,6 +3,7 @@ import { ConfigYaml, createPromptMarkdown, createRuleMarkdown, + sanitizeRuleName, } from "@continuedev/config-yaml"; import * as YAML from "yaml"; import { IDE } from "../.."; @@ -116,13 +117,31 @@ export async function findAvailableFilename( fileExists: (uri: string) => Promise, extension?: string, isGlobal?: boolean, + baseFilenameOverride?: string, ): Promise { - // 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; @@ -141,6 +160,7 @@ export async function findAvailableFilename( export async function createNewWorkspaceBlockFile( ide: IDE, blockType: BlockType, + baseFilename?: string, ): Promise { const workspaceDirs = await ide.getWorkspaceDirs(); if (workspaceDirs.length === 0) { @@ -155,6 +175,9 @@ export async function createNewWorkspaceBlockFile( baseDirUri, blockType, ide.fileExists.bind(ide), + undefined, + false, + baseFilename, ); const fileContent = getFileContent(blockType); @@ -163,7 +186,10 @@ export async function createNewWorkspaceBlockFile( await ide.openFile(fileUri); } -export async function createNewGlobalRuleFile(ide: IDE): Promise { +export async function createNewGlobalRuleFile( + ide: IDE, + baseFilename?: string, +): Promise { try { const globalDir = localPathToUri(getContinueGlobalPath()); @@ -176,6 +202,7 @@ export async function createNewGlobalRuleFile(ide: IDE): Promise { ide.fileExists.bind(ide), undefined, true, // isGlobal = true for global rules + baseFilename, ); const fileContent = getFileContent("rules"); diff --git a/core/core.ts b/core/core.ts index c73996a4951..2985c04860c 100644 --- a/core/core.ts +++ b/core/core.ts @@ -409,7 +409,11 @@ 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)", ); @@ -417,7 +421,7 @@ export class Core { 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)", ); diff --git a/core/protocol/core.ts b/core/protocol/core.ts index b03b0e11a77..bfa85870987 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -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]; diff --git a/gui/src/components/dialogs/AddRuleDialog.tsx b/gui/src/components/dialogs/AddRuleDialog.tsx new file mode 100644 index 00000000000..49a6e672348 --- /dev/null +++ b/gui/src/components/dialogs/AddRuleDialog.tsx @@ -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("new-rule"); + const [error, setError] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const inputRef = useRef(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 ( +
+
+

{title}

+

+ Choose a name for the new rule file. +

+
+ + {error &&

{error}

} +
+ + Create + + + Cancel + +
+
+
+
+ ); +} + +export default AddRuleDialog; diff --git a/gui/src/pages/config/sections/RulesSection.tsx b/gui/src/pages/config/sections/RulesSection.tsx index 4e9741bb51c..08bdb6da775 100644 --- a/gui/src/pages/config/sections/RulesSection.tsx +++ b/gui/src/pages/config/sections/RulesSection.tsx @@ -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 { @@ -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("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( + , + ), + ); } else { void ideMessenger.request("controlPlane/openUrl", { path: "?type=rules",