Merge branch 'main' into Azure

This commit is contained in:
Iskandar Sulaili 2025-11-21 08:02:20 +08:00 committed by GitHub
commit d527f2fdab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 9942 additions and 130 deletions

View file

@ -18,7 +18,7 @@
"@types/vscode": "^1.95.0",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.4.0",
"glob": "^11.0.1",
"glob": "^11.1.0",
"mocha": "^11.1.0",
"rimraf": "^6.0.1",
"typescript": "5.8.3"

View file

@ -34,7 +34,7 @@
"@vscode/vsce": "3.3.2",
"esbuild": "^0.25.0",
"eslint": "^9.27.0",
"glob": "^11.0.3",
"glob": "^11.1.0",
"husky": "^9.1.7",
"knip": "^5.44.4",
"lint-staged": "^16.0.0",
@ -54,12 +54,13 @@
},
"pnpm": {
"overrides": {
"tar-fs": ">=2.1.3",
"tar-fs": ">=3.1.1",
"esbuild": ">=0.25.0",
"undici": ">=5.29.0",
"brace-expansion": ">=2.0.2",
"form-data": ">=4.0.4",
"bluebird": ">=3.7.2"
"bluebird": ">=3.7.2",
"glob": ">=11.1.0"
}
}
}

View file

@ -0,0 +1,20 @@
/**
* Image generation model constants
*/
export interface ImageGenerationModel {
value: string
label: string
}
export const IMAGE_GENERATION_MODELS: ImageGenerationModel[] = [
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image" },
{ value: "google/gemini-3-pro-image-preview", label: "Gemini 3 Pro Image Preview" },
{ value: "openai/gpt-5-image", label: "GPT-5 Image" },
{ value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini" },
]
/**
* Get array of model values only (for backend validation)
*/
export const IMAGE_GENERATION_MODEL_IDS = IMAGE_GENERATION_MODELS.map((m) => m.value)

View file

@ -7,6 +7,7 @@ export * from "./experiment.js"
export * from "./followup.js"
export * from "./global-settings.js"
export * from "./history.js"
export * from "./image-generation.js"
export * from "./ipc.js"
export * from "./marketplace.js"
export * from "./mcp.js"

9607
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -244,6 +244,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
strict?: boolean
}>
tool_choice?: any
parallel_tool_calls?: boolean
}
// Validate requested tier against model support; if not supported, omit.
@ -302,6 +303,12 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
}
// For native tool protocol, explicitly disable parallel tool calls.
// For XML or when protocol is unset, omit the field entirely so the API default applies.
if (metadata?.toolProtocol === "native") {
body.parallel_tool_calls = false
}
// Include text.verbosity only when the model explicitly supports it
if (model.info.supportsVerbosity === true) {
body.text = { verbosity: (verbosity || "medium") as VerbosityLevel }

View file

@ -10,7 +10,7 @@ import insertContent from "./insert_content"
import listCodeDefinitionNames from "./list_code_definition_names"
import listFiles from "./list_files"
import newTask from "./new_task"
import { read_file } from "./read_file"
import { createReadFileTool } from "./read_file"
import runSlashCommand from "./run_slash_command"
import searchFiles from "./search_files"
import switchMode from "./switch_mode"
@ -21,23 +21,34 @@ import { apply_diff_single_file } from "./apply_diff"
export { getMcpServerTools } from "./mcp_server"
export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters"
export const nativeTools = [
apply_diff_single_file,
askFollowupQuestion,
attemptCompletion,
browserAction,
codebaseSearch,
executeCommand,
fetchInstructions,
generateImage,
insertContent,
listCodeDefinitionNames,
listFiles,
newTask,
read_file,
runSlashCommand,
searchFiles,
switchMode,
updateTodoList,
writeToFile,
] satisfies OpenAI.Chat.ChatCompletionTool[]
/**
* Get native tools array, optionally customizing based on settings.
*
* @param partialReadsEnabled - Whether to include line_ranges support in read_file tool (default: true)
* @returns Array of native tool definitions
*/
export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool[] {
return [
apply_diff_single_file,
askFollowupQuestion,
attemptCompletion,
browserAction,
codebaseSearch,
executeCommand,
fetchInstructions,
generateImage,
insertContent,
listCodeDefinitionNames,
listFiles,
newTask,
createReadFileTool(partialReadsEnabled),
runSlashCommand,
searchFiles,
switchMode,
updateTodoList,
writeToFile,
] satisfies OpenAI.Chat.ChatCompletionTool[]
}
// Backward compatibility: export default tools with line ranges enabled
export const nativeTools = getNativeTools(true)

View file

@ -1,43 +1,80 @@
import type OpenAI from "openai"
export const read_file = {
type: "function",
function: {
name: "read_file",
description:
"Read one or more files and return their contents with line numbers for diffing or discussion. Use line ranges when available to keep reads efficient and combine related files when possible.",
strict: true,
parameters: {
type: "object",
properties: {
files: {
type: "array",
description: "List of files to read; request related files together when allowed",
items: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the file to read, relative to the workspace",
},
line_ranges: {
type: ["array", "null"],
description:
"Optional 1-based inclusive ranges to read (format: start-end). Use multiple ranges for non-contiguous sections and keep ranges tight to the needed context.",
items: {
type: "string",
pattern: "^[0-9]+-[0-9]+$",
},
},
},
required: ["path"],
additionalProperties: false,
},
minItems: 1,
},
},
required: ["files"],
additionalProperties: false,
/**
* Creates the read_file tool definition, optionally including line_ranges support
* based on whether partial reads are enabled.
*
* @param partialReadsEnabled - Whether to include line_ranges parameter
* @returns Native tool definition for read_file
*/
export function createReadFileTool(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool {
const baseDescription =
"Read one or more files and return their contents with line numbers for diffing or discussion. " +
"Structure: { files: [{ path: 'relative/path.ts'" +
(partialReadsEnabled ? ", line_ranges: ['1-50', '100-150']" : "") +
" }] }. " +
"The 'path' is required and relative to workspace. "
const optionalRangesDescription = partialReadsEnabled
? "The 'line_ranges' is optional for reading specific sections (format: 'start-end', 1-based inclusive). "
: ""
const examples = partialReadsEnabled
? "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
"Example with line ranges: { files: [{ path: 'src/app.ts', line_ranges: ['1-50', '100-150'] }] }. " +
"Example multiple files: { files: [{ path: 'file1.ts', line_ranges: ['1-50'] }, { path: 'file2.ts' }] }"
: "Example single file: { files: [{ path: 'src/app.ts' }] }. " +
"Example multiple files: { files: [{ path: 'file1.ts' }, { path: 'file2.ts' }] }"
const description = baseDescription + optionalRangesDescription + examples
// Build the properties object conditionally
const fileProperties: Record<string, any> = {
path: {
type: "string",
description: "Path to the file to read, relative to the workspace",
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
}
// Only include line_ranges if partial reads are enabled
if (partialReadsEnabled) {
fileProperties.line_ranges = {
type: ["array", "null"],
description:
"Optional 1-based inclusive ranges to read (format: start-end). Use multiple ranges for non-contiguous sections and keep ranges tight to the needed context.",
items: {
type: "string",
pattern: "^[0-9]+-[0-9]+$",
},
}
}
return {
type: "function",
function: {
name: "read_file",
description,
strict: true,
parameters: {
type: "object",
properties: {
files: {
type: "array",
description: "List of files to read; request related files together when allowed",
items: {
type: "object",
properties: fileProperties,
required: ["path"],
additionalProperties: false,
},
minItems: 1,
},
},
required: ["files"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
}
export const read_file = createReadFileTool(false)

View file

@ -87,8 +87,7 @@ import { getWorkspacePath } from "../../utils/path"
// prompts
import { formatResponse } from "../prompts/responses"
import { SYSTEM_PROMPT } from "../prompts/system"
import { nativeTools, getMcpServerTools } from "../prompts/tools/native-tools"
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../prompts/tools/filter-tools-for-mode"
import { buildNativeToolsArray } from "./build-tools"
// core modules
import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
@ -2025,9 +2024,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails)
// Remove any existing environment_details blocks before adding fresh ones.
// This prevents duplicate environment details when resuming tasks with XML tool calls,
// where the old user message content may already contain environment details from the previous session.
// We check for both opening and closing tags to ensure we're matching complete environment detail blocks,
// not just mentions of the tag in regular content.
const contentWithoutEnvDetails = parsedUserContent.filter((block) => {
if (block.type === "text" && typeof block.text === "string") {
// Check if this text block is a complete environment_details block
// by verifying it starts with the opening tag and ends with the closing tag
const isEnvironmentDetailsBlock =
block.text.trim().startsWith("<environment_details>") &&
block.text.trim().endsWith("</environment_details>")
return !isEnvironmentDetailsBlock
}
return true
})
// Add environment details as its own text block, separate from tool
// results.
const finalUserContent = [...parsedUserContent, { type: "text" as const, text: environmentDetails }]
const finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }]
// Only add user message to conversation history if:
// 1. This is the first attempt (retryAttempt === 0), OR
@ -3132,34 +3148,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
let allTools: OpenAI.Chat.ChatCompletionTool[] = []
if (shouldIncludeTools) {
const provider = this.providerRef.deref()
const mcpHub = provider?.getMcpHub()
// Get CodeIndexManager for feature checking
const { CodeIndexManager } = await import("../../services/code-index/manager")
const codeIndexManager = CodeIndexManager.getInstance(provider!.context, this.cwd)
// Build settings object for tool filtering
// Include browserToolEnabled to filter browser_action when disabled by user
const filterSettings = {
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
browserToolEnabled: state?.browserToolEnabled ?? true,
if (!provider) {
throw new Error("Provider reference lost during tool building")
}
// Filter native tools based on mode restrictions (similar to XML tool filtering)
const filteredNativeTools = filterNativeToolsForMode(
nativeTools,
allTools = await buildNativeToolsArray({
provider,
cwd: this.cwd,
mode,
state?.customModes,
state?.experiments,
codeIndexManager,
filterSettings,
)
// Filter MCP tools based on mode restrictions
const mcpTools = getMcpServerTools(mcpHub)
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, state?.customModes, state?.experiments)
allTools = [...filteredNativeTools, ...filteredMcpTools]
customModes: state?.customModes,
experiments: state?.experiments,
apiConfiguration,
maxReadFileLine: state?.maxReadFileLine ?? -1,
browserToolEnabled: state?.browserToolEnabled ?? true,
})
}
const metadata: ApiHandlerCreateMessageMetadata = {

View file

@ -196,5 +196,126 @@ describe("Task Tool History Handling", () => {
content: '{"setting": "value"}',
})
})
describe("environment details deduplication", () => {
it("should filter out existing environment_details blocks before adding new ones", () => {
// Simulate user content that already contains environment details from a previous session
const userContentWithOldEnvDetails = [
{
type: "text" as const,
text: "Some user message",
},
{
type: "text" as const,
text: "<environment_details>\n# Old Environment Details\nCurrent time: 2024-01-01\n</environment_details>",
},
]
// Filter out existing environment_details blocks using the same logic as Task.ts
const contentWithoutEnvDetails = userContentWithOldEnvDetails.filter((block) => {
if (block.type === "text" && typeof block.text === "string") {
// Check if this text block is a complete environment_details block
const isEnvironmentDetailsBlock =
block.text.trim().startsWith("<environment_details>") &&
block.text.trim().endsWith("</environment_details>")
return !isEnvironmentDetailsBlock
}
return true
})
// Verify old environment details were removed
expect(contentWithoutEnvDetails).toHaveLength(1)
expect(contentWithoutEnvDetails[0].text).toBe("Some user message")
// Simulate adding fresh environment details
const newEnvironmentDetails =
"<environment_details>\n# Fresh Environment Details\nCurrent time: 2024-01-02\n</environment_details>"
const finalUserContent = [
...contentWithoutEnvDetails,
{ type: "text" as const, text: newEnvironmentDetails },
]
// Verify we have exactly one environment_details block (the new one)
const envDetailsBlocks = finalUserContent.filter((block) => {
if (block.type === "text" && typeof block.text === "string") {
return (
block.text.trim().startsWith("<environment_details>") &&
block.text.trim().endsWith("</environment_details>")
)
}
return false
})
expect(envDetailsBlocks).toHaveLength(1)
expect(envDetailsBlocks[0].text).toContain("2024-01-02")
expect(envDetailsBlocks[0].text).not.toContain("2024-01-01")
})
it("should not filter out text that mentions environment_details tags in content", () => {
// User content that mentions the tags but isn't an environment_details block
const userContent = [
{
type: "text" as const,
text: "Let me explain how <environment_details> work in this system",
},
{
type: "text" as const,
text: "The closing tag is </environment_details>",
},
{
type: "text" as const,
text: "Regular message",
},
]
// Filter using the same logic as Task.ts
const contentWithoutEnvDetails = userContent.filter((block) => {
if (block.type === "text" && typeof block.text === "string") {
const isEnvironmentDetailsBlock =
block.text.trim().startsWith("<environment_details>") &&
block.text.trim().endsWith("</environment_details>")
return !isEnvironmentDetailsBlock
}
return true
})
// All blocks should be preserved since none are complete environment_details blocks
expect(contentWithoutEnvDetails).toHaveLength(3)
expect(contentWithoutEnvDetails).toEqual(userContent)
})
it("should not filter out regular text blocks", () => {
// User content with various blocks but no environment details
const userContent = [
{
type: "text" as const,
text: "Regular message",
},
{
type: "text" as const,
text: "Another message with <task> tags",
},
{
type: "tool_result" as const,
tool_use_id: "tool_123",
content: "Tool result",
},
]
// Filter using the same logic as Task.ts
const contentWithoutEnvDetails = userContent.filter((block) => {
if (block.type === "text" && typeof block.text === "string") {
const isEnvironmentDetailsBlock =
block.text.trim().startsWith("<environment_details>") &&
block.text.trim().endsWith("</environment_details>")
return !isEnvironmentDetailsBlock
}
return true
})
// All blocks should be preserved
expect(contentWithoutEnvDetails).toHaveLength(3)
expect(contentWithoutEnvDetails).toEqual(userContent)
})
})
})
})

View file

@ -0,0 +1,62 @@
import type OpenAI from "openai"
import type { ProviderSettings, ModeConfig } from "@roo-code/types"
import type { ClineProvider } from "../webview/ClineProvider"
import { getNativeTools, getMcpServerTools } from "../prompts/tools/native-tools"
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../prompts/tools/filter-tools-for-mode"
interface BuildToolsOptions {
provider: ClineProvider
cwd: string
mode: string | undefined
customModes: ModeConfig[] | undefined
experiments: Record<string, boolean> | undefined
apiConfiguration: ProviderSettings | undefined
maxReadFileLine: number
browserToolEnabled: boolean
}
/**
* Builds the complete tools array for native protocol requests.
* Combines native tools and MCP tools, filtered by mode restrictions.
*
* @param options - Configuration options for building the tools
* @returns Array of filtered native and MCP tools
*/
export async function buildNativeToolsArray(options: BuildToolsOptions): Promise<OpenAI.Chat.ChatCompletionTool[]> {
const { provider, cwd, mode, customModes, experiments, apiConfiguration, maxReadFileLine, browserToolEnabled } =
options
const mcpHub = provider.getMcpHub()
// Get CodeIndexManager for feature checking
const { CodeIndexManager } = await import("../../services/code-index/manager")
const codeIndexManager = CodeIndexManager.getInstance(provider.context, cwd)
// Build settings object for tool filtering
const filterSettings = {
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
browserToolEnabled: browserToolEnabled ?? true,
}
// Determine if partial reads are enabled based on maxReadFileLine setting
const partialReadsEnabled = maxReadFileLine !== -1
// Build native tools with dynamic read_file tool based on partialReadsEnabled
const nativeTools = getNativeTools(partialReadsEnabled)
// Filter native tools based on mode restrictions
const filteredNativeTools = filterNativeToolsForMode(
nativeTools,
mode,
customModes,
experiments,
codeIndexManager,
filterSettings,
)
// Filter MCP tools based on mode restrictions
const mcpTools = getMcpServerTools(mcpHub)
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments)
return [...filteredNativeTools, ...filteredMcpTools]
}

View file

@ -1,7 +1,7 @@
import path from "path"
import fs from "fs/promises"
import * as vscode from "vscode"
import type { GenerateImageParams } from "@roo-code/types"
import { GenerateImageParams, IMAGE_GENERATION_MODEL_IDS } from "@roo-code/types"
import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
import { fileExistsAtPath } from "../../utils/fs"
@ -12,8 +12,6 @@ import { OpenRouterHandler } from "../../api/providers/openrouter"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
const IMAGE_GENERATION_MODELS = ["google/gemini-2.5-flash-image", "openai/gpt-5-image", "openai/gpt-5-image-mini"]
export class GenerateImageTool extends BaseTool<"generate_image"> {
readonly name = "generate_image" as const
@ -137,7 +135,7 @@ export class GenerateImageTool extends BaseTool<"generate_image"> {
return
}
const selectedModel = state?.openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODELS[0]
const selectedModel = state?.openRouterImageGenerationSelectedModel || IMAGE_GENERATION_MODEL_IDS[0]
const fullPath = path.resolve(task.cwd, removeClosingTag("path", relPath))
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)

View file

@ -559,7 +559,7 @@
"@vscode/vsce": "3.3.2",
"esbuild": "^0.25.0",
"execa": "^9.5.2",
"glob": "^11.0.1",
"glob": "^11.1.0",
"mkdirp": "^3.0.1",
"nock": "^14.0.4",
"npm-run-all2": "^8.0.1",

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react"
import { VSCodeCheckbox, VSCodeTextField, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { IMAGE_GENERATION_MODELS } from "@roo-code/types"
import { useAppTranslation } from "@/i18n/TranslationContext"
interface ImageGenerationSettingsProps {
@ -11,14 +12,6 @@ interface ImageGenerationSettingsProps {
setImageGenerationSelectedModel: (model: string) => void
}
// Hardcoded list of image generation models
const IMAGE_GENERATION_MODELS = [
{ value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image" },
{ value: "openai/gpt-5-image", label: "GPT-5 Image" },
{ value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini" },
// Add more models as they become available
]
export const ImageGenerationSettings = ({
enabled,
onChange,