Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 58ca3c2

Browse files
ctemrubensellipsis-dev[bot]
authored
Custom tools UI (#10244)
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 021b4c5 commit 58ca3c2

31 files changed

Lines changed: 431 additions & 15 deletions

packages/types/src/experiment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const experimentIds = [
1313
"imageGeneration",
1414
"runSlashCommand",
1515
"multipleNativeToolCalls",
16+
"customTools",
1617
] as const
1718

1819
export const experimentIdsSchema = z.enum(experimentIds)
@@ -30,6 +31,7 @@ export const experimentsSchema = z.object({
3031
imageGeneration: z.boolean().optional(),
3132
runSlashCommand: z.boolean().optional(),
3233
multipleNativeToolCalls: z.boolean().optional(),
34+
customTools: z.boolean().optional(),
3335
})
3436

3537
export type Experiments = z.infer<typeof experimentsSchema>

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,17 +1081,15 @@ export async function presentAssistantMessage(cline: Task) {
10811081
break
10821082
}
10831083

1084-
const customTool = customToolRegistry.get(block.name)
1084+
const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined
10851085

10861086
if (customTool) {
10871087
try {
1088-
console.log(`executing customTool -> ${JSON.stringify(customTool, null, 2)}`)
10891088
let customToolArgs
10901089

10911090
if (customTool.parameters) {
10921091
try {
10931092
customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {})
1094-
console.log(`customToolArgs -> ${JSON.stringify(customToolArgs, null, 2)}`)
10951093
} catch (parseParamsError) {
10961094
const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}`
10971095
console.error(message)
@@ -1102,13 +1100,15 @@ export async function presentAssistantMessage(cline: Task) {
11021100
}
11031101
}
11041102

1105-
console.log(`${customTool.name}.execute() -> ${JSON.stringify(customToolArgs, null, 2)}`)
1106-
11071103
const result = await customTool.execute(customToolArgs, {
11081104
mode: mode ?? defaultModeSlug,
11091105
task: cline,
11101106
})
11111107

1108+
console.log(
1109+
`${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`,
1110+
)
1111+
11121112
pushToolResult(result)
11131113
cline.consecutiveMistakeCount = 0
11141114
} catch (executionError: any) {

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ async function generatePrompt(
124124

125125
let customToolsSection = ""
126126

127-
if (!isNativeProtocol(effectiveProtocol)) {
127+
if (experiments?.customTools && !isNativeProtocol(effectiveProtocol)) {
128128
const customTools = customToolRegistry.getAllSerialized()
129129

130130
if (customTools.length > 0) {

src/core/task/build-tools.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,16 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
7979
const mcpTools = getMcpServerTools(mcpHub)
8080
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments)
8181

82-
// Add custom tools if they are available.
83-
await customToolRegistry.loadFromDirectoryIfStale(path.join(cwd, ".roo", "tools"))
84-
const customTools = customToolRegistry.getAllSerialized()
82+
// Add custom tools if they are available and the experiment is enabled.
8583
let nativeCustomTools: OpenAI.Chat.ChatCompletionFunctionTool[] = []
8684

87-
if (customTools.length > 0) {
88-
nativeCustomTools = customTools.map(formatNative)
85+
if (experiments?.customTools) {
86+
await customToolRegistry.loadFromDirectoryIfStale(path.join(cwd, ".roo", "tools"))
87+
const customTools = customToolRegistry.getAllSerialized()
88+
89+
if (customTools.length > 0) {
90+
nativeCustomTools = customTools.map(formatNative)
91+
}
8992
}
9093

9194
return [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools]

src/core/tools/validateToolUse.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../shared/tools"
1111
* Note: This does NOT check if the tool is allowed for a specific mode,
1212
* only that the tool actually exists.
1313
*/
14-
export function isValidToolName(toolName: string): toolName is ToolName {
14+
export function isValidToolName(toolName: string, experiments?: Record<string, boolean>): toolName is ToolName {
1515
// Check if it's a valid static tool
1616
if ((validToolNames as readonly string[]).includes(toolName)) {
1717
return true
1818
}
1919

20-
if (customToolRegistry.has(toolName)) {
20+
if (experiments?.customTools && customToolRegistry.has(toolName)) {
2121
return true
2222
}
2323

@@ -40,7 +40,7 @@ export function validateToolUse(
4040
): void {
4141
// First, check if the tool name is actually a valid/known tool
4242
// This catches completely invalid tool names like "edit_file" that don't exist
43-
if (!isValidToolName(toolName)) {
43+
if (!isValidToolName(toolName, experiments)) {
4444
throw new Error(
4545
`Unknown tool "${toolName}". This tool does not exist. Please use one of the available tools: ${validToolNames.join(", ")}.`,
4646
)
@@ -94,7 +94,7 @@ export function isToolAllowedForMode(
9494

9595
// For now, allow all custom tools in any mode.
9696
// As a follow-up we should expand the custom tool definition to include mode restrictions.
97-
if (customToolRegistry.has(tool)) {
97+
if (experiments?.customTools && customToolRegistry.has(tool)) {
9898
return true
9999
}
100100

src/core/webview/webviewMessageHandler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Experiments,
1717
ExperimentId,
1818
} from "@roo-code/types"
19+
import { customToolRegistry } from "@roo-code/core"
1920
import { CloudService } from "@roo-code/cloud"
2021
import { TelemetryService } from "@roo-code/telemetry"
2122

@@ -1725,6 +1726,24 @@ export const webviewMessageHandler = async (
17251726
}
17261727
break
17271728
}
1729+
case "refreshCustomTools": {
1730+
try {
1731+
await customToolRegistry.loadFromDirectory(path.join(getCurrentCwd(), ".roo", "tools"))
1732+
1733+
await provider.postMessageToWebview({
1734+
type: "customToolsResult",
1735+
tools: customToolRegistry.getAllSerialized(),
1736+
})
1737+
} catch (error) {
1738+
await provider.postMessageToWebview({
1739+
type: "customToolsResult",
1740+
tools: [],
1741+
error: error instanceof Error ? error.message : String(error),
1742+
})
1743+
}
1744+
1745+
break
1746+
}
17281747
case "saveApiConfiguration":
17291748
if (message.text && message.apiConfiguration) {
17301749
try {

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
OrganizationAllowList,
1515
ShareVisibility,
1616
QueuedMessage,
17+
SerializedCustomToolDefinition,
1718
} from "@roo-code/types"
1819

1920
import { GitCommit } from "../utils/git"
@@ -133,6 +134,7 @@ export interface ExtensionMessage {
133134
| "browserSessionUpdate"
134135
| "browserSessionNavigate"
135136
| "claudeCodeRateLimits"
137+
| "customToolsResult"
136138
text?: string
137139
payload?: any // Add a generic payload for now, can refine later
138140
// Checkpoint warning message
@@ -218,6 +220,7 @@ export interface ExtensionMessage {
218220
browserSessionMessages?: ClineMessage[] // For browser session panel updates
219221
isBrowserSessionActive?: boolean // For browser session panel updates
220222
stepIndex?: number // For browserSessionNavigate: the target step index to display
223+
tools?: SerializedCustomToolDefinition[] // For customToolsResult
221224
}
222225

223226
export type ExtensionState = Pick<

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export interface WebviewMessage {
180180
| "openDebugUiHistory"
181181
| "downloadErrorDiagnostics"
182182
| "requestClaudeCodeRateLimits"
183+
| "refreshCustomTools"
183184
text?: string
184185
editedMessageContent?: string
185186
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

src/shared/__tests__/experiments.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe("experiments", () => {
3232
imageGeneration: false,
3333
runSlashCommand: false,
3434
multipleNativeToolCalls: false,
35+
customTools: false,
3536
}
3637
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
3738
})
@@ -44,6 +45,7 @@ describe("experiments", () => {
4445
imageGeneration: false,
4546
runSlashCommand: false,
4647
multipleNativeToolCalls: false,
48+
customTools: false,
4749
}
4850
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
4951
})
@@ -56,6 +58,7 @@ describe("experiments", () => {
5658
imageGeneration: false,
5759
runSlashCommand: false,
5860
multipleNativeToolCalls: false,
61+
customTools: false,
5962
}
6063
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
6164
})

src/shared/experiments.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const EXPERIMENT_IDS = {
77
IMAGE_GENERATION: "imageGeneration",
88
RUN_SLASH_COMMAND: "runSlashCommand",
99
MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls",
10+
CUSTOM_TOOLS: "customTools",
1011
} as const satisfies Record<string, ExperimentId>
1112

1213
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@@ -24,6 +25,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
2425
IMAGE_GENERATION: { enabled: false },
2526
RUN_SLASH_COMMAND: { enabled: false },
2627
MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false },
28+
CUSTOM_TOOLS: { enabled: false },
2729
}
2830

2931
export const experimentDefault = Object.fromEntries(

0 commit comments

Comments
 (0)