Skip to content
Open
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
2 changes: 1 addition & 1 deletion core/config/json/loadRcConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function getWorkspaceRcConfigs(
entry[0].endsWith(".continuerc.json"),
)
.map((entry) => joinPathsToUri(dir, entry[0]));
return await Promise.all(rcFiles.map(ide.readFile));
return await Promise.all(rcFiles.map((uri) => ide.readFile(uri)));
}),
);
return rcFiles
Expand Down
2 changes: 2 additions & 0 deletions core/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
},
slashCommands: (a: any, b: any) => a.name === b.name,
customCommands: (a: any, b: any) => a.name === b.name,
tools: (a: any, b: any) => a.name === b.name,
};

function loadSerializedConfig(
Expand Down Expand Up @@ -464,7 +465,7 @@
}
if (name === "llm") {
const llm = models.find((model) => model.title === params?.modelTitle);
if (!llm) {

Check warning on line 468 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
errors.push({
fatal: false,
message: `Unknown reranking model ${params?.modelTitle}`,
Expand Down Expand Up @@ -504,6 +505,7 @@
...config,
contextProviders,
tools: getBaseToolDefinitions(),
toolOverrides: config.tools, // Pass through tool overrides from config
mcpServerStatuses: [],
slashCommands: [],
modelsByRole: {
Expand Down Expand Up @@ -558,7 +560,7 @@
id: `continue-mcp-server-${index + 1}`,
name: `MCP Server`,
requestOptions: mergeConfigYamlRequestOptions(
server.transport.type !== "stdio"

Check warning on line 563 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
? server.transport.requestOptions
: undefined,
config.requestOptions,
Expand Down
28 changes: 28 additions & 0 deletions core/config/profile/doLoadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js";
import ContinueProxy from "../../llm/llms/stubs/ContinueProxy";
import { initSlashCommand } from "../../promptFiles/initPrompt";
import { getConfigDependentToolDefinitions } from "../../tools";
import { applyToolOverrides } from "../../tools/applyToolOverrides";
import { encodeMCPToolUri } from "../../tools/callTool";
import { getMCPToolName } from "../../tools/mcpToolName";
import { GlobalContext } from "../../util/GlobalContext";
Expand All @@ -39,6 +40,7 @@ import { Telemetry } from "../../util/posthog";
import { SentryLogger } from "../../util/sentry/SentryLogger";
import { TTS } from "../../util/tts";
import { getWorkspaceContinueRuleDotFiles } from "../getWorkspaceContinueRuleDotFiles";
import { getWorkspaceRcConfigs } from "../json/loadRcConfigs";
import { loadContinueConfigFromJson } from "../load";
import { CodebaseRulesCache } from "../markdown/loadCodebaseRules";
import { loadMarkdownRules } from "../markdown/loadMarkdownRules";
Expand Down Expand Up @@ -309,6 +311,32 @@ export default async function doLoadConfig(options: {
}),
);

// Load workspace .continuerc.json files for tool overrides
// (JSON path loads these earlier during config merge, YAML path doesn't)
if (!newConfig.toolOverrides?.length) {
const workspaceRcConfigs = await getWorkspaceRcConfigs(ide);
for (const rcConfig of workspaceRcConfigs) {
if (rcConfig.tools?.length) {
newConfig.toolOverrides = [
...(newConfig.toolOverrides ?? []),
...rcConfig.tools,
];
}
}
}

// Apply tool overrides from config (e.g., .continuerc.json)
if (newConfig.toolOverrides?.length) {
const toolOverridesResult = applyToolOverrides(
newConfig.tools,
newConfig.toolOverrides,
);
newConfig.tools = toolOverridesResult.tools;
errors.push(...toolOverridesResult.errors);
// Clear toolOverrides after applying (not needed at runtime)
delete newConfig.toolOverrides;
}

// Detect duplicate tool names
const counts: Record<string, number> = {};
newConfig.tools.forEach((tool) => {
Expand Down
38 changes: 34 additions & 4 deletions core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,31 @@ declare global {
url?: string;
clientKey?: string;
}


/**
* Configuration for overriding built-in tool prompts.
* Allows customization of tool descriptions and behavior at the repo level.
*/
export interface ToolOverride {
/** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */
name: string;
/** Override the tool's description shown to the LLM */
description?: string;
/** Override the display title shown in UI */
displayTitle?: string;
/** Override the action phrases */
wouldLikeTo?: string;
isCurrently?: string;
hasAlready?: string;
/** Override system message description for non-native tool calling */
systemMessageDescription?: {
prefix?: string;
exampleArgs?: Array<[string, string | number]>;
};
/** Set to true to disable this tool */
disabled?: boolean;
}

// config.json
export interface SerializedContinueConfig {
env?: string[];
Expand All @@ -1156,8 +1180,10 @@ declare global {
experimental?: ExperimentalConfig;
analytics?: AnalyticsConfig;
docs?: SiteIndexingConfig[];
/** Tool overrides for customizing built-in tool prompts at the repo level */
tools?: ToolOverride[];
}

export type ConfigMergeType = "merge" | "overwrite";

export type ContinueRcJson = Partial<SerializedContinueConfig> & {
Expand Down Expand Up @@ -1208,8 +1234,10 @@ declare global {
experimental?: ExperimentalConfig;
/** Analytics configuration */
analytics?: AnalyticsConfig;
/** Tool overrides for customizing built-in tool prompts */
tools?: ToolOverride[];
}

// in the actual Continue source code
export interface ContinueConfig {
allowAnonymousTelemetry?: boolean;
Expand All @@ -1231,8 +1259,10 @@ declare global {
analytics?: AnalyticsConfig;
docs?: SiteIndexingConfig[];
tools: Tool[];
/** Tool overrides from config, applied after all tools are loaded */
toolOverrides?: ToolOverride[];
}

export interface BrowserSerializedContinueConfig {
allowAnonymousTelemetry?: boolean;
models: ModelDescription[];
Expand Down
30 changes: 30 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,30 @@ export interface Tool {
) => ToolPolicy;
}

/**
* Configuration for overriding built-in tool prompts.
* Allows customization of tool descriptions and behavior at the repo level.
*/
export interface ToolOverride {
/** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */
name: string;
/** Override the tool's description shown to the LLM */
description?: string;
/** Override the display title shown in UI */
displayTitle?: string;
/** Override the action phrases */
wouldLikeTo?: string;
isCurrently?: string;
hasAlready?: string;
/** Override system message description for non-native tool calling */
systemMessageDescription?: {
prefix?: string;
exampleArgs?: Array<[string, string | number]>;
};
/** Set to true to disable this tool */
disabled?: boolean;
}

interface ToolChoice {
type: "function";
function: {
Expand Down Expand Up @@ -1721,6 +1745,8 @@ export interface SerializedContinueConfig {
analytics?: AnalyticsConfig;
docs?: SiteIndexingConfig[];
data?: DataDestination[];
/** Tool overrides for customizing built-in tool prompts at the repo level */
tools?: ToolOverride[];
}

export type ConfigMergeType = "merge" | "overwrite";
Expand Down Expand Up @@ -1775,6 +1801,8 @@ export interface Config {
analytics?: AnalyticsConfig;
docs?: SiteIndexingConfig[];
data?: DataDestination[];
/** Tool overrides for customizing built-in tool prompts */
tools?: ToolOverride[];
}

// in the actual Continue source code
Expand All @@ -1794,6 +1822,8 @@ export interface ContinueConfig {
analytics?: AnalyticsConfig;
docs?: SiteIndexingConfig[];
tools: Tool[];
/** Tool overrides from config, applied after all tools are loaded */
toolOverrides?: ToolOverride[];
mcpServerStatuses: MCPServerStatus[];
rules: RuleWithSource[];
modelsByRole: Record<ModelRole, ILLM[]>;
Expand Down
184 changes: 184 additions & 0 deletions core/tools/applyToolOverrides.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { Tool, ToolOverride } from "..";
import { applyToolOverrides } from "./applyToolOverrides";

const mockTool = (name: string, description: string): Tool => ({
type: "function",
displayTitle: name,
readonly: true,
group: "test",
function: { name, description },
});

describe("applyToolOverrides", () => {
it("should return tools unchanged when no overrides provided", () => {
const tools = [mockTool("read_file", "Read a file")];
const result = applyToolOverrides(tools, undefined);
expect(result.tools).toEqual(tools);
expect(result.errors).toHaveLength(0);
});

it("should return tools unchanged when empty overrides array provided", () => {
const tools = [mockTool("read_file", "Read a file")];
const result = applyToolOverrides(tools, []);
expect(result.tools).toEqual(tools);
expect(result.errors).toHaveLength(0);
});

it("should override description when specified", () => {
const tools = [mockTool("read_file", "Original description")];
const overrides: ToolOverride[] = [
{ name: "read_file", description: "New description" },
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].function.description).toBe("New description");
expect(result.errors).toHaveLength(0);
});

it("should override displayTitle when specified", () => {
const tools = [mockTool("read_file", "Read a file")];
const overrides: ToolOverride[] = [
{ name: "read_file", displayTitle: "Custom Read File" },
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].displayTitle).toBe("Custom Read File");
});

it("should override action phrases when specified", () => {
const tools = [mockTool("read_file", "Read a file")];
tools[0].wouldLikeTo = "read {{{ filepath }}}";
tools[0].isCurrently = "reading {{{ filepath }}}";
tools[0].hasAlready = "read {{{ filepath }}}";

const overrides: ToolOverride[] = [
{
name: "read_file",
wouldLikeTo: "open {{{ filepath }}}",
isCurrently: "opening {{{ filepath }}}",
hasAlready: "opened {{{ filepath }}}",
},
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].wouldLikeTo).toBe("open {{{ filepath }}}");
expect(result.tools[0].isCurrently).toBe("opening {{{ filepath }}}");
expect(result.tools[0].hasAlready).toBe("opened {{{ filepath }}}");
});

it("should disable tools when disabled: true", () => {
const tools = [
mockTool("read_file", "Read"),
mockTool("write_file", "Write"),
];
const overrides: ToolOverride[] = [{ name: "read_file", disabled: true }];
const result = applyToolOverrides(tools, overrides);
expect(result.tools).toHaveLength(1);
expect(result.tools[0].function.name).toBe("write_file");
expect(result.errors).toHaveLength(0);
});

it("should warn when override references unknown tool", () => {
const tools = [mockTool("read_file", "Read")];
const overrides: ToolOverride[] = [
{ name: "unknown_tool", description: "test" },
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools).toHaveLength(1);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].message).toContain("unknown_tool");
expect(result.errors[0].fatal).toBe(false);
});

it("should preserve unmodified fields", () => {
const tools = [mockTool("read_file", "Original")];
tools[0].readonly = true;
tools[0].group = "Built-In";

const overrides: ToolOverride[] = [
{ name: "read_file", description: "New description" },
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].readonly).toBe(true);
expect(result.tools[0].group).toBe("Built-In");
expect(result.tools[0].displayTitle).toBe("read_file");
});

it("should override systemMessageDescription", () => {
const tools = [mockTool("read_file", "Read")];
tools[0].systemMessageDescription = {
prefix: "old prefix",
exampleArgs: [["filepath", "/old/path"]],
};

const overrides: ToolOverride[] = [
{
name: "read_file",
systemMessageDescription: {
prefix: "new prefix",
exampleArgs: [["filepath", "/new/path"]],
},
},
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix");
expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([
["filepath", "/new/path"],
]);
});

it("should partially override systemMessageDescription", () => {
const tools = [mockTool("read_file", "Read")];
tools[0].systemMessageDescription = {
prefix: "old prefix",
exampleArgs: [["filepath", "/old/path"]],
};

const overrides: ToolOverride[] = [
{
name: "read_file",
systemMessageDescription: {
prefix: "new prefix",
// exampleArgs not specified - should preserve original
},
},
];
const result = applyToolOverrides(tools, overrides);
expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix");
expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([
["filepath", "/old/path"],
]);
});

it("should apply multiple overrides", () => {
const tools = [
mockTool("read_file", "Read"),
mockTool("write_file", "Write"),
mockTool("delete_file", "Delete"),
];

const overrides: ToolOverride[] = [
{ name: "read_file", description: "Custom read" },
{ name: "write_file", disabled: true },
{ name: "delete_file", displayTitle: "Remove File" },
];

const result = applyToolOverrides(tools, overrides);
expect(result.tools).toHaveLength(2);
expect(result.tools[0].function.description).toBe("Custom read");
expect(result.tools[1].displayTitle).toBe("Remove File");
expect(result.errors).toHaveLength(0);
});

it("should not mutate original tools array", () => {
const tools = [mockTool("read_file", "Original")];
const originalDescription = tools[0].function.description;

const overrides: ToolOverride[] = [
{ name: "read_file", description: "New description" },
];
const result = applyToolOverrides(tools, overrides);

// Original should be unchanged
expect(tools[0].function.description).toBe(originalDescription);
// Result should have new description
expect(result.tools[0].function.description).toBe("New description");
});
});
Loading
Loading