Merge branch 'main' into vs/support-unbound

This commit is contained in:
pugazhendhi-m
2025-01-28 21:58:23 +05:30
committed by GitHub
41 changed files with 1691 additions and 180 deletions

View File

@@ -6,6 +6,7 @@ import { McpServer } from "./mcp"
import { GitCommit } from "../utils/git"
import { Mode, CustomModePrompts, ModeConfig } from "./modes"
import { CustomSupportPrompts } from "./support-prompt"
import { ExperimentId } from "./experiments"
export interface LanguageModelChatSelector {
vendor?: string
@@ -91,6 +92,7 @@ export interface ExtensionState {
alwaysAllowBrowser?: boolean
alwaysAllowMcp?: boolean
alwaysApproveResubmit?: boolean
alwaysAllowModeSwitch?: boolean
requestDelaySeconds: number
uriScheme?: string
allowedCommands?: string[]
@@ -107,7 +109,7 @@ export interface ExtensionState {
mode: Mode
modeApiConfigs?: Record<Mode, string>
enhancementApiConfigId?: string
experimentalDiffStrategy?: boolean
experiments: Record<ExperimentId, boolean> // Map of experiment IDs to their enabled state
autoApprovalEnabled?: boolean
customModes: ModeConfig[]
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)

View File

@@ -41,6 +41,7 @@ export interface WebviewMessage {
| "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "playSound"
| "soundEnabled"
| "soundVolume"
@@ -74,7 +75,7 @@ export interface WebviewMessage {
| "getSystemPrompt"
| "systemPrompt"
| "enhancementApiConfigId"
| "experimentalDiffStrategy"
| "updateExperimental"
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"

View File

@@ -14,6 +14,12 @@ describe("isToolAllowedForMode", () => {
roleDefinition: "You are a CSS editor",
groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
},
{
slug: "test-exp-mode",
name: "Test Exp Mode",
roleDefinition: "You are an experimental tester",
groups: ["read", "edit", "browser"],
},
]
it("allows always available tools", () => {
@@ -240,6 +246,87 @@ describe("isToolAllowedForMode", () => {
expect(isToolAllowedForMode("write_to_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
})
describe("experimental tools", () => {
it("disables tools when experiment is disabled", () => {
const experiments = {
search_and_replace: false,
insert_code_block: false,
}
expect(
isToolAllowedForMode(
"search_and_replace",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(false)
expect(
isToolAllowedForMode(
"insert_code_block",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(false)
})
it("allows tools when experiment is enabled", () => {
const experiments = {
search_and_replace: true,
insert_code_block: true,
}
expect(
isToolAllowedForMode(
"search_and_replace",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(true)
expect(
isToolAllowedForMode(
"insert_code_block",
"test-exp-mode",
customModes,
undefined,
undefined,
experiments,
),
).toBe(true)
})
it("allows non-experimental tools when experiments are disabled", () => {
const experiments = {
search_and_replace: false,
insert_code_block: false,
}
expect(
isToolAllowedForMode("read_file", "markdown-editor", customModes, undefined, undefined, experiments),
).toBe(true)
expect(
isToolAllowedForMode(
"write_to_file",
"markdown-editor",
customModes,
undefined,
{ path: "test.md" },
experiments,
),
).toBe(true)
})
})
})
describe("FileRestrictionError", () => {

69
src/shared/experiments.ts Normal file
View File

@@ -0,0 +1,69 @@
export const EXPERIMENT_IDS = {
DIFF_STRATEGY: "experimentalDiffStrategy",
SEARCH_AND_REPLACE: "search_and_replace",
INSERT_BLOCK: "insert_code_block",
} as const
export type ExperimentKey = keyof typeof EXPERIMENT_IDS
export type ExperimentId = valueof<typeof EXPERIMENT_IDS>
export interface ExperimentConfig {
name: string
description: string
enabled: boolean
}
type valueof<X> = X[keyof X]
export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
DIFF_STRATEGY: {
name: "Use experimental unified diff strategy",
description:
"Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits. Only enable if you understand the risks and are willing to carefully review all changes.",
enabled: false,
},
SEARCH_AND_REPLACE: {
name: "Use experimental search and replace tool",
description:
"Enable the experimental search and replace tool, allowing Roo to replace multiple instances of a search term in one request.",
enabled: false,
},
INSERT_BLOCK: {
name: "Use experimental insert block tool",
description:
"Enable the experimental insert block tool, allowing Roo to insert multiple code blocks at once at specific line numbers without needing to create a diff.",
enabled: false,
},
}
export const experimentDefault = Object.fromEntries(
Object.entries(experimentConfigsMap).map(([_, config]) => [
EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId,
config.enabled,
]),
) as Record<ExperimentId, boolean>
export const experiments = {
get: (id: ExperimentKey): ExperimentConfig | undefined => {
return experimentConfigsMap[id]
},
isEnabled: (experimentsConfig: Record<ExperimentId, boolean>, id: ExperimentId): boolean => {
return experimentsConfig[id] ?? experimentDefault[id]
},
} as const
// Expose experiment details for UI - pre-compute from map for better performance
export const experimentLabels = Object.fromEntries(
Object.entries(experimentConfigsMap).map(([_, config]) => [
EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId,
config.name,
]),
) as Record<string, string>
export const experimentDescriptions = Object.fromEntries(
Object.entries(experimentConfigsMap).map(([_, config]) => [
EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId,
config.description,
]),
) as Record<string, string>

View File

@@ -6,6 +6,7 @@ interface ApiMetrics {
totalCacheWrites?: number
totalCacheReads?: number
totalCost: number
contextTokens: number // Total tokens in conversation (last message's tokensIn + tokensOut)
}
/**
@@ -32,8 +33,22 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
totalCacheWrites: undefined,
totalCacheReads: undefined,
totalCost: 0,
contextTokens: 0,
}
// Find the last api_req_started message that has valid token information
const lastApiReq = [...messages].reverse().find((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
const parsedData = JSON.parse(message.text)
return typeof parsedData.tokensIn === "number" && typeof parsedData.tokensOut === "number"
} catch {
return false
}
}
return false
})
messages.forEach((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
@@ -55,6 +70,14 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
if (typeof cost === "number") {
result.totalCost += cost
}
// If this is the last api request, use its tokens for context size
if (message === lastApiReq) {
// Only update context tokens if both input and output tokens are non-zero
if (tokensIn > 0 && tokensOut > 0) {
result.contextTokens = tokensIn + tokensOut
}
}
} catch (error) {
console.error("Error parsing JSON:", error)
}

View File

@@ -160,12 +160,19 @@ export function isToolAllowedForMode(
customModes: ModeConfig[],
toolRequirements?: Record<string, boolean>,
toolParams?: Record<string, any>, // All tool parameters
experiments?: Record<string, boolean>,
): boolean {
// Always allow these tools
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
return true
}
if (experiments && tool in experiments) {
if (!experiments[tool]) {
return false
}
}
// Check tool requirements if any exist
if (toolRequirements && tool in toolRequirements) {
if (!toolRequirements[tool]) {
@@ -198,7 +205,7 @@ export function isToolAllowedForMode(
const filePath = toolParams?.path
if (
filePath &&
(toolParams.diff || toolParams.content) &&
(toolParams.diff || toolParams.content || toolParams.operations) &&
!doesFileMatchRegex(filePath, options.fileRegex)
) {
throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath)

View File

@@ -21,7 +21,7 @@ export const TOOL_DISPLAY_NAMES = {
// Define available tool groups
export const TOOL_GROUPS: Record<string, ToolGroupValues> = {
read: ["read_file", "search_files", "list_files", "list_code_definition_names"],
edit: ["write_to_file", "apply_diff"],
edit: ["write_to_file", "apply_diff", "insert_code_block", "search_and_replace"],
browser: ["browser_action"],
command: ["execute_command"],
mcp: ["use_mcp_tool", "access_mcp_resource"],