From 0df5ad48058e827ea316f77ca50f84563bb6a83d Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sat, 12 Jul 2025 02:42:02 -0700 Subject: [PATCH 1/5] codebase rules cache --- core/config/markdown/loadCodebaseRules.ts | 40 ++++++++++++++++++- .../markdown/loadCodebaseRules.vitest.ts | 8 ++-- .../ruleCollocationApplication.vitest.ts | 18 ++++----- core/config/profile/doLoadConfig.ts | 9 ++--- core/core.ts | 26 ++++++++---- core/index.d.ts | 1 + core/llm/rules/ruleColocation.vitest.ts | 18 ++++----- core/protocol/core.ts | 2 +- .../mainInput/belowMainInput/RulesPeek.tsx | 2 + 9 files changed, 88 insertions(+), 36 deletions(-) diff --git a/core/config/markdown/loadCodebaseRules.ts b/core/config/markdown/loadCodebaseRules.ts index a239f56f1c..1b1ba29194 100644 --- a/core/config/markdown/loadCodebaseRules.ts +++ b/core/config/markdown/loadCodebaseRules.ts @@ -7,6 +7,40 @@ import { walkDirs } from "../../indexing/walkDir"; import { RULES_MARKDOWN_FILENAME } from "../../llm/rules/constants"; import { getUriPathBasename } from "../../util/uri"; +export class CodebaseRulesCache { + private static instance: CodebaseRulesCache | null = null; + private constructor() {} + + public static getInstance(): CodebaseRulesCache { + if (CodebaseRulesCache.instance === null) { + CodebaseRulesCache.instance = new CodebaseRulesCache(); + } + return CodebaseRulesCache.instance; + } + rules: RuleWithSource[] = []; + errors: ConfigValidationError[] = []; + async refresh(ide: IDE) { + const { rules, errors } = await loadCodebaseRules(ide); + this.rules = rules; + this.errors = errors; + } + async update(ide: IDE, uri: string) { + const content = await ide.readFile(uri); + const rule = markdownToRule(content, { uriType: "file", filePath: uri }); + const ruleWithSource: RuleWithSource = { + ...rule, + source: "colocated-markdown", + ruleFile: uri, + }; + const matchIdx = this.rules.findIndex((r) => r.ruleFile === uri); + if (matchIdx === -1) { + this.rules.push(ruleWithSource); + } else { + this.rules[matchIdx] = ruleWithSource; + } + } +} + /** * Loads rules from rules.md files colocated in the codebase */ @@ -33,7 +67,11 @@ export async function loadCodebaseRules(ide: IDE): Promise<{ const content = await ide.readFile(filePath); const rule = markdownToRule(content, { uriType: "file", filePath }); - rules.push({ ...rule, source: "rules-block", ruleFile: filePath }); + rules.push({ + ...rule, + source: "colocated-markdown", + ruleFile: filePath, + }); } catch (e) { errors.push({ fatal: false, diff --git a/core/config/markdown/loadCodebaseRules.vitest.ts b/core/config/markdown/loadCodebaseRules.vitest.ts index 9a77f692b4..f9c3972c87 100644 --- a/core/config/markdown/loadCodebaseRules.vitest.ts +++ b/core/config/markdown/loadCodebaseRules.vitest.ts @@ -45,27 +45,27 @@ describe("loadCodebaseRules", () => { "src/rules.md": { name: "General Rules", rule: "Follow coding standards", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/rules.md", }, "src/redux/rules.md": { name: "Redux Rules", rule: "Use Redux Toolkit", globs: "**/*.{ts,tsx}", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/redux/rules.md", }, "src/components/rules.md": { name: "Component Rules", rule: "Use functional components", globs: ["**/*.tsx", "**/*.jsx"], - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", }, ".continue/rules.md": { name: "Global Rules", rule: "Follow project guidelines", - source: "rules-block", + source: "colocated-markdown", ruleFile: ".continue/rules.md", }, }; diff --git a/core/config/markdown/ruleCollocationApplication.vitest.ts b/core/config/markdown/ruleCollocationApplication.vitest.ts index 35210870df..120e2d6a17 100644 --- a/core/config/markdown/ruleCollocationApplication.vitest.ts +++ b/core/config/markdown/ruleCollocationApplication.vitest.ts @@ -16,7 +16,7 @@ describe("Rule Colocation Application", () => { { name: "Root Rule", rule: "Follow project standards", - source: "rules-block", + source: "colocated-markdown", ruleFile: ".continue/rules.md", }, @@ -24,7 +24,7 @@ describe("Rule Colocation Application", () => { { name: "React Components Rule", rule: "Use functional components with hooks", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", // No explicit globs - should implicitly only apply to files in that directory }, @@ -34,7 +34,7 @@ describe("Rule Colocation Application", () => { name: "Redux Rule", rule: "Use Redux Toolkit for state management", globs: "src/redux/**/*.{ts,tsx}", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/redux/rules.md", }, @@ -43,7 +43,7 @@ describe("Rule Colocation Application", () => { name: "TypeScript Components Rule", rule: "Use TypeScript with React components", globs: "**/*.tsx", // Only apply to .tsx files - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", }, @@ -52,7 +52,7 @@ describe("Rule Colocation Application", () => { name: "API Utils Rule", rule: "Follow API utility conventions", globs: "**/*.ts", // Only TypeScript files in this directory - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/utils/api/rules.md", }, ]; @@ -183,7 +183,7 @@ describe("Rule Colocation Application", () => { const impliedComponentRule: RuleWithSource = { name: "Implied Components Rule", rule: "Use React component best practices", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", // No explicit globs - should infer from directory }; @@ -233,7 +233,7 @@ describe("Rule Colocation Application", () => { name: "TypeScript Component Rule", rule: "Use TypeScript with React components", globs: "**/*.tsx", // Only apply to .tsx files - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", }; @@ -280,7 +280,7 @@ describe("Rule Colocation Application", () => { name: "API Utils Rule", rule: "Follow API utility conventions", globs: "**/*.ts", // Only TypeScript files in this directory - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/utils/api/rules.md", }; @@ -323,7 +323,7 @@ describe("Rule Colocation Application", () => { return { name: `Inferred Rule for ${directory}`, rule: `Follow standards for ${directory}`, - source: "rules-block", + source: "colocated-markdown", ruleFile: ruleFilePath, // In a fixed implementation, these globs would be automatically inferred // globs: expectedGlob, diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index 1a5d27ab95..858085bf93 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -37,7 +37,7 @@ import { Telemetry } from "../../util/posthog"; import { TTS } from "../../util/tts"; import { getWorkspaceContinueRuleDotFiles } from "../getWorkspaceContinueRuleDotFiles"; import { loadContinueConfigFromJson } from "../load"; -import { loadCodebaseRules } from "../markdown/loadCodebaseRules"; +import { CodebaseRulesCache } from "../markdown/loadCodebaseRules"; import { loadMarkdownRules } from "../markdown/loadMarkdownRules"; import { migrateJsonSharedConfig } from "../migrateSharedConfig"; import { rectifySelectedModelsFromGlobalContext } from "../selectedModels"; @@ -165,10 +165,9 @@ export default async function doLoadConfig(options: { } // Add rules from colocated rules.md files in the codebase - const { rules: codebaseRules, errors: codebaseRulesErrors } = - await loadCodebaseRules(ide); - newConfig.rules.unshift(...codebaseRules); - errors.push(...codebaseRulesErrors); + const codebaseRulesCache = CodebaseRulesCache.getInstance(); + newConfig.rules.unshift(...codebaseRulesCache.rules); + errors.push(...codebaseRulesCache.errors); // Rectify model selections for each role newConfig = rectifySelectedModelsFromGlobalContext(newConfig, profileId); diff --git a/core/core.ts b/core/core.ts index 915099a3e4..a2d4c9e747 100644 --- a/core/core.ts +++ b/core/core.ts @@ -52,6 +52,7 @@ import { BLOCK_TYPES, ConfigYaml } from "@continuedev/config-yaml"; import { getDiffFn, GitDiffCache } from "./autocomplete/snippets/gitDiffCache"; import { stringifyMcpPrompt } from "./commands/slash/mcpSlashCommand"; import { isLocalDefinitionFile } from "./config/loadLocalAssistants"; +import { CodebaseRulesCache } from "./config/markdown/loadCodebaseRules"; import { setupLocalConfig, setupProviderConfig, @@ -233,6 +234,14 @@ export class Core { (..._) => Promise.resolve([]), ); + const codebaseRulesCache = CodebaseRulesCache.getInstance(); + codebaseRulesCache + .refresh(ide) + .catch((e) => console.error("Failed to initialize colocated rules cache")) + .then(() => { + this.configHandler.reloadConfig(); + }); + this.registerMessageHandlers(ideSettingsPromise); } @@ -323,8 +332,10 @@ export class Core { }); on("config/reload", async (msg) => { + // User force reloading will retrigger colocated rules + const codebaseRulesCache = CodebaseRulesCache.getInstance(); + await codebaseRulesCache.refresh(this.ide); void this.configHandler.reloadConfig(); - return await this.configHandler.getSerializedConfig(); }); on("config/ideSettingsUpdate", async (msg) => { @@ -966,15 +977,16 @@ export class Core { uri.endsWith(".continuerc.json") || uri.endsWith(".prompt") || uri.endsWith(SYSTEM_PROMPT_DOT_FILE) || - (uri.includes(".continue") && uri.endsWith(".yaml")) || - uri.endsWith(RULES_MARKDOWN_FILENAME) || - BLOCK_TYPES.some( - (blockType) => - uri.includes(`.continue/${blockType}`) || - uri.includes(`.continue\\${blockType}`), + (uri.includes(".continue") && + (uri.endsWith(".yaml") || uri.endsWith("yml"))) || + BLOCK_TYPES.some((blockType) => + uri.includes(`.continue/${blockType}`), ) ) { await this.configHandler.reloadConfig(); + } else if (uri.endsWith(RULES_MARKDOWN_FILENAME)) { + const codebaseRulesCache = CodebaseRulesCache.getInstance(); + void codebaseRulesCache.update(this.ide, uri); } else if ( uri.endsWith(".continueignore") || uri.endsWith(".gitignore") diff --git a/core/index.d.ts b/core/index.d.ts index 014dcc6de5..b25214d4a8 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1650,6 +1650,7 @@ export type RuleSource = | "model-options-plan" | "model-options-agent" | "rules-block" + | "colocated-markdown" | "json-systemMessage" | ".continuerules"; diff --git a/core/llm/rules/ruleColocation.vitest.ts b/core/llm/rules/ruleColocation.vitest.ts index 4b5530a8ee..6bee635b0f 100644 --- a/core/llm/rules/ruleColocation.vitest.ts +++ b/core/llm/rules/ruleColocation.vitest.ts @@ -9,7 +9,7 @@ describe("Rule colocation - glob pattern matching", () => { generalRule: { name: "General Rule", rule: "Follow coding standards", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/rules.md", }, @@ -18,7 +18,7 @@ describe("Rule colocation - glob pattern matching", () => { name: "Redux Rule", rule: "Use Redux Toolkit", globs: "src/redux/**/*.{ts,tsx}", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/redux/rules.md", }, @@ -27,7 +27,7 @@ describe("Rule colocation - glob pattern matching", () => { name: "Component Rule", rule: "Use functional components", globs: ["src/components/**/*.tsx", "src/components/**/*.jsx"], - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/components/rules.md", }, @@ -37,7 +37,7 @@ describe("Rule colocation - glob pattern matching", () => { rule: "Follow these guidelines always", alwaysApply: true, globs: "src/specific/**/*.ts", // Should be ignored since alwaysApply is true - source: "rules-block", + source: "colocated-markdown", ruleFile: ".continue/rules.md", }, @@ -47,7 +47,7 @@ describe("Rule colocation - glob pattern matching", () => { rule: "This rule should only apply to matching files", alwaysApply: false, // No globs, so should never apply - source: "rules-block", + source: "colocated-markdown", ruleFile: ".continue/rules.md", }, @@ -57,7 +57,7 @@ describe("Rule colocation - glob pattern matching", () => { rule: "Apply only to matching files", alwaysApply: false, globs: "src/utils/**/*.ts", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/utils/rules.md", }, }; @@ -127,7 +127,7 @@ describe("Rule colocation - glob pattern matching", () => { rule: "Follow nested module standards", // Fix: Specify the exact path prefix to restrict to this directory structure globs: "src/features/auth/utils/**/*.ts", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/features/auth/utils/rules.md", }; @@ -157,7 +157,7 @@ describe("Rule colocation - glob pattern matching", () => { // Note: Negative globs may not be supported by the current implementation // Testing with standard pattern instead globs: "src/**/[!.]*.ts", - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/rules.md", }; @@ -174,7 +174,7 @@ describe("Rule colocation - glob pattern matching", () => { // Use a pattern that doesn't match test files globs: ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.spec.ts"], alwaysApply: false, - source: "rules-block", + source: "colocated-markdown", ruleFile: "src/rules.md", }; diff --git a/core/protocol/core.ts b/core/protocol/core.ts index f06d1db042..7d0b4455bd 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -89,7 +89,7 @@ export type ToCoreFromIdeOrWebviewProtocol = { }, ]; "config/deleteModel": [{ title: string }, void]; - "config/reload": [undefined, ConfigResult]; + "config/reload": [undefined, void]; "config/refreshProfiles": [ ( | undefined diff --git a/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx b/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx index c9c60acc75..b649edde68 100644 --- a/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx +++ b/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx @@ -27,6 +27,8 @@ const getSourceLabel = (source: RuleSource): string => { return "Model Agent Options"; case "rules-block": return "Rules Block"; + case "colocated-markdown": + return "Codebase Rule"; case "json-systemMessage": return "System Message"; case ".continuerules": From c123f8032ae10bb167faddb4b108ef63b1d3309b Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Sat, 12 Jul 2025 02:45:17 -0700 Subject: [PATCH 2/5] reload config on codebase rule update --- core/core.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/core.ts b/core/core.ts index a2d4c9e747..8ec2cd515e 100644 --- a/core/core.ts +++ b/core/core.ts @@ -985,8 +985,13 @@ export class Core { ) { await this.configHandler.reloadConfig(); } else if (uri.endsWith(RULES_MARKDOWN_FILENAME)) { - const codebaseRulesCache = CodebaseRulesCache.getInstance(); - void codebaseRulesCache.update(this.ide, uri); + try { + const codebaseRulesCache = CodebaseRulesCache.getInstance(); + await codebaseRulesCache.update(this.ide, uri); + this.configHandler.reloadConfig(); + } catch (e) { + console.error("Failed to update codebase rule", e); + } } else if ( uri.endsWith(".continueignore") || uri.endsWith(".gitignore") From e483d4dca8a9ce5825ef657748d4f4462c01d581 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 17 Jul 2025 15:26:39 +0200 Subject: [PATCH 3/5] void codebase rule promise --- core/core.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/core.ts b/core/core.ts index d05bd51c77..61db83dc94 100644 --- a/core/core.ts +++ b/core/core.ts @@ -694,6 +694,10 @@ export class Core { void refreshIfNotIgnored(data.uris); if (hasRulesFiles(data.uris)) { + const rulesCache = CodebaseRulesCache.getInstance(); + await Promise.all( + data.uris.map((uri) => rulesCache.update(this.ide, uri)), + ); await this.configHandler.reloadConfig("Rules file created"); } @@ -1046,7 +1050,7 @@ export class Core { } else if (uri.endsWith(RULES_MARKDOWN_FILENAME)) { try { const codebaseRulesCache = CodebaseRulesCache.getInstance(); - codebaseRulesCache.update(this.ide, uri).then(() => { + void codebaseRulesCache.update(this.ide, uri).then(() => { this.configHandler.reloadConfig("Codebase rule update"); }); } catch (e) { From da1707444e99dff09201895665b077f4703e7c1b Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 17 Jul 2025 15:27:51 +0200 Subject: [PATCH 4/5] void rules cache promises part 2 --- core/core.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/core.ts b/core/core.ts index 61db83dc94..9a34da3bb0 100644 --- a/core/core.ts +++ b/core/core.ts @@ -237,11 +237,11 @@ export class Core { ); const codebaseRulesCache = CodebaseRulesCache.getInstance(); - codebaseRulesCache + void codebaseRulesCache .refresh(ide) .catch((e) => console.error("Failed to initialize colocated rules cache")) .then(() => { - this.configHandler.reloadConfig( + void this.configHandler.reloadConfig( "Initial codebase rules post-walkdir/load reload", ); }); @@ -700,7 +700,6 @@ export class Core { ); await this.configHandler.reloadConfig("Rules file created"); } - // If it's a local assistant being created, we want to reload all assistants so it shows up in the list let localAssistantCreated = false; for (const uri of data.uris) { @@ -1051,7 +1050,7 @@ export class Core { try { const codebaseRulesCache = CodebaseRulesCache.getInstance(); void codebaseRulesCache.update(this.ide, uri).then(() => { - this.configHandler.reloadConfig("Codebase rule update"); + void this.configHandler.reloadConfig("Codebase rule update"); }); } catch (e) { console.error("Failed to update codebase rule", e); From 69c75754222afdabe4850433123b01eaf25a299c Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 17 Jul 2025 15:34:10 +0200 Subject: [PATCH 5/5] better colocated rule source label --- .../mainInput/belowMainInput/RulesPeek.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx b/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx index b649edde68..c887e9d9e3 100644 --- a/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx +++ b/gui/src/components/mainInput/belowMainInput/RulesPeek.tsx @@ -1,5 +1,6 @@ import { DocumentTextIcon, GlobeAltIcon } from "@heroicons/react/24/outline"; -import { RuleSource, RuleWithSource } from "core"; +import { RuleWithSource } from "core"; +import { getLastNPathParts } from "core/util/uri"; import { ComponentType, useMemo, useState } from "react"; import ToggleDiv from "../../ToggleDiv"; @@ -13,8 +14,8 @@ interface RulesPeekItemProps { } // Convert technical source to user-friendly text -const getSourceLabel = (source: RuleSource): string => { - switch (source) { +const getSourceLabel = (rule: RuleWithSource): string => { + switch (rule.source) { case "default-chat": return "Default Chat"; case "default-agent": @@ -28,13 +29,17 @@ const getSourceLabel = (source: RuleSource): string => { case "rules-block": return "Rules Block"; case "colocated-markdown": - return "Codebase Rule"; + if (rule.ruleFile) { + return getLastNPathParts(rule.ruleFile, 2); + } else { + return "rules.md"; + } case "json-systemMessage": return "System Message"; case ".continuerules": return "Project Rules"; default: - return source; + return rule.source; } }; @@ -97,7 +102,7 @@ export function RulesPeekItem({ rule }: RulesPeekItemProps) { )}
- Source: {getSourceLabel(rule.source)} + Source: {getSourceLabel(rule)}
);