Skip to content

Codebase/Colocated Rules Cache #6603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 17, 2025
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
43 changes: 42 additions & 1 deletion core/config/markdown/loadCodebaseRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ 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;
}
}
remove(uri: string) {
this.rules = this.rules.filter((r) => r.ruleFile !== uri);
}
}

/**
* Loads rules from rules.md files colocated in the codebase
*/
Expand All @@ -33,7 +70,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,
Expand Down
8 changes: 4 additions & 4 deletions core/config/markdown/loadCodebaseRules.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
};
Expand Down
18 changes: 9 additions & 9 deletions core/config/markdown/ruleCollocationApplication.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ describe("Rule Colocation Application", () => {
{
name: "Root Rule",
rule: "Follow project standards",
source: "rules-block",
source: "colocated-markdown",
ruleFile: ".continue/rules.md",
},

// Nested directory rule without globs - should only apply to files in that directory
{
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
},
Expand All @@ -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",
},

Expand All @@ -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",
},

Expand All @@ -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",
},
];
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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",
};

Expand Down Expand Up @@ -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",
};

Expand Down Expand Up @@ -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,
Expand Down
9 changes: 4 additions & 5 deletions core/config/profile/doLoadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 34 additions & 10 deletions core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -69,11 +70,11 @@ import { RULES_MARKDOWN_FILENAME } from "./llm/rules/constants";
import { llmStreamChat } from "./llm/streamChat";
import { BeforeAfterDiff } from "./nextEdit/context/diffFormatting";
import { processSmallEdit } from "./nextEdit/context/processSmallEdit";
import { NextEditProvider } from "./nextEdit/NextEditProvider";
import type { FromCoreProtocol, ToCoreProtocol } from "./protocol";
import { OnboardingModes } from "./protocol/core";
import type { IMessenger, Message } from "./protocol/messenger";
import { getUriPathBasename } from "./util/uri";
import { NextEditProvider } from "./nextEdit/NextEditProvider";

const hasRulesFiles = (uris: string[]): boolean => {
for (const uri of uris) {
Expand Down Expand Up @@ -235,6 +236,15 @@ export class Core {
(..._) => Promise.resolve([]),
);

const codebaseRulesCache = CodebaseRulesCache.getInstance();
void codebaseRulesCache
.refresh(ide)
.catch((e) => console.error("Failed to initialize colocated rules cache"))
.then(() => {
void this.configHandler.reloadConfig(
"Initial codebase rules post-walkdir/load reload",
);
});
this.nextEditProvider = NextEditProvider.initialize(
this.configHandler,
ide,
Expand Down Expand Up @@ -342,10 +352,12 @@ 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(
"Force reloaded (config/reload message)",
);
return await this.configHandler.getSerializedConfig();
});

on("config/ideSettingsUpdate", async (msg) => {
Expand Down Expand Up @@ -682,9 +694,12 @@ 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");
}

// 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) {
Expand All @@ -704,7 +719,9 @@ export class Core {
void refreshIfNotIgnored(data.uris);

if (hasRulesFiles(data.uris)) {
await this.configHandler.reloadConfig("Rules file deleted");
const rulesCache = CodebaseRulesCache.getInstance();
data.uris.forEach((uri) => rulesCache.remove(uri));
await this.configHandler.reloadConfig("Codebase rule file deleted");
}
}
});
Expand Down Expand Up @@ -1020,17 +1037,24 @@ 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(
"Config-related file updated: continuerc, prompt, local block, etc",
);
} else if (uri.endsWith(RULES_MARKDOWN_FILENAME)) {
try {
const codebaseRulesCache = CodebaseRulesCache.getInstance();
void codebaseRulesCache.update(this.ide, uri).then(() => {
void this.configHandler.reloadConfig("Codebase rule update");
});
} catch (e) {
console.error("Failed to update codebase rule", e);
}
} else if (
uri.endsWith(".continueignore") ||
uri.endsWith(".gitignore")
Expand Down
1 change: 1 addition & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,7 @@ export type RuleSource =
| "model-options-plan"
| "model-options-agent"
| "rules-block"
| "colocated-markdown"
| "json-systemMessage"
| ".continuerules";

Expand Down
18 changes: 9 additions & 9 deletions core/llm/rules/ruleColocation.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},

Expand All @@ -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",
},

Expand All @@ -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",
},

Expand All @@ -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",
},

Expand All @@ -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",
},

Expand All @@ -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",
},
};
Expand Down Expand Up @@ -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",
};

Expand Down Expand Up @@ -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",
};

Expand All @@ -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",
};

Expand Down
2 changes: 1 addition & 1 deletion core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
},
];
"config/deleteModel": [{ title: string }, void];
"config/reload": [undefined, ConfigResult<BrowserSerializedContinueConfig>];
"config/reload": [undefined, void];
"config/refreshProfiles": [
(
| undefined
Expand Down
Loading
Loading