mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge 77b934b6ab into b867ec9145
This commit is contained in:
commit
03af821153
10 changed files with 596 additions and 9 deletions
|
|
@ -50,6 +50,8 @@ export const codebaseIndexConfigSchema = z.object({
|
|||
codebaseIndexBedrockProfile: z.string().optional(),
|
||||
// OpenRouter specific fields
|
||||
codebaseIndexOpenRouterSpecificProvider: z.string().optional(),
|
||||
// WarpGrep specific fields
|
||||
warpGrepEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
|
||||
|
|
@ -85,6 +87,7 @@ export const codebaseIndexProviderSchema = z.object({
|
|||
codebaseIndexMistralApiKey: z.string().optional(),
|
||||
codebaseIndexVercelAiGatewayApiKey: z.string().optional(),
|
||||
codebaseIndexOpenRouterApiKey: z.string().optional(),
|
||||
warpGrepApiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexProvider = z.infer<typeof codebaseIndexProviderSchema>
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ export const SECRET_STATE_KEYS = [
|
|||
"codebaseIndexMistralApiKey",
|
||||
"codebaseIndexVercelAiGatewayApiKey",
|
||||
"codebaseIndexOpenRouterApiKey",
|
||||
"warpGrepApiKey",
|
||||
"sambaNovaApiKey",
|
||||
"zaiApiKey",
|
||||
"fireworksApiKey",
|
||||
|
|
|
|||
|
|
@ -614,6 +614,10 @@ export interface WebviewMessage {
|
|||
codebaseIndexMistralApiKey?: string
|
||||
codebaseIndexVercelAiGatewayApiKey?: string
|
||||
codebaseIndexOpenRouterApiKey?: string
|
||||
|
||||
// WarpGrep specific fields
|
||||
warpGrepEnabled?: boolean
|
||||
warpGrepApiKey?: string
|
||||
}
|
||||
updatedSettings?: RooCodeSettings
|
||||
/** Task configuration applied via `createTask()`. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
import { filterNativeToolsForMode, isToolAllowedInMode } from "../filter-tools-for-mode"
|
||||
|
||||
function makeTool(name: string): OpenAI.Chat.ChatCompletionTool {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
description: `${name} tool`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
} as OpenAI.Chat.ChatCompletionTool
|
||||
}
|
||||
|
||||
describe("filterNativeToolsForMode - WarpGrep", () => {
|
||||
it("keeps codebase_search available when WarpGrep is enabled", () => {
|
||||
const result = filterNativeToolsForMode(
|
||||
[makeTool("codebase_search"), makeTool("read_file")],
|
||||
"code",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
codebaseIndexConfig: {
|
||||
warpGrepEnabled: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.map((tool) => (tool as any).function.name)).toContain("codebase_search")
|
||||
})
|
||||
|
||||
it("reports codebase_search as allowed when WarpGrep is enabled", () => {
|
||||
expect(
|
||||
isToolAllowedInMode("codebase_search", "code", undefined, undefined, undefined, {
|
||||
codebaseIndexConfig: {
|
||||
warpGrepEnabled: true,
|
||||
},
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -269,9 +269,16 @@ export function filterNativeToolsForMode(
|
|||
allowedToolNames = customizedTools
|
||||
|
||||
// Conditionally exclude codebase_search if feature is disabled or not configured
|
||||
// WarpGrep can serve as an alternative backend for codebase_search
|
||||
const warpGrepEnabled = settings?.codebaseIndexConfig?.warpGrepEnabled === true
|
||||
if (
|
||||
!codeIndexManager ||
|
||||
!(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized)
|
||||
!warpGrepEnabled &&
|
||||
(!codeIndexManager ||
|
||||
!(
|
||||
codeIndexManager.isFeatureEnabled &&
|
||||
codeIndexManager.isFeatureConfigured &&
|
||||
codeIndexManager.isInitialized
|
||||
))
|
||||
) {
|
||||
allowedToolNames.delete("codebase_search")
|
||||
}
|
||||
|
|
@ -363,11 +370,15 @@ export function isToolAllowedInMode(
|
|||
if (ALWAYS_AVAILABLE_TOOLS.includes(toolName)) {
|
||||
// But still check for conditional exclusions
|
||||
if (toolName === "codebase_search") {
|
||||
return !!(
|
||||
codeIndexManager &&
|
||||
codeIndexManager.isFeatureEnabled &&
|
||||
codeIndexManager.isFeatureConfigured &&
|
||||
codeIndexManager.isInitialized
|
||||
const warpGrepEnabled = settings?.codebaseIndexConfig?.warpGrepEnabled === true
|
||||
return (
|
||||
warpGrepEnabled ||
|
||||
!!(
|
||||
codeIndexManager &&
|
||||
codeIndexManager.isFeatureEnabled &&
|
||||
codeIndexManager.isFeatureConfigured &&
|
||||
codeIndexManager.isInitialized
|
||||
)
|
||||
)
|
||||
}
|
||||
if (toolName === "update_todo_list") {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
|
|||
// Build settings object for tool filtering.
|
||||
const filterSettings = {
|
||||
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
|
||||
codebaseIndexConfig: provider.contextProxy.getGlobalState("codebaseIndexConfig"),
|
||||
disabledTools,
|
||||
modelInfo,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { CodeIndexManager } from "../../services/code-index/manager"
|
|||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { VectorStoreSearchResult } from "../../services/code-index/interfaces"
|
||||
import { executeWarpGrepSearch } from "../../services/warpgrep"
|
||||
import type { ToolUse } from "../../shared/tools"
|
||||
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
|
@ -52,7 +53,32 @@ export class CodebaseSearchTool extends BaseTool<"codebase_search"> {
|
|||
task.consecutiveMistakeCount = 0
|
||||
|
||||
try {
|
||||
const context = task.providerRef.deref()?.context
|
||||
const provider = task.providerRef.deref()
|
||||
const contextProxy = provider?.contextProxy
|
||||
|
||||
// Try WarpGrep first if enabled
|
||||
if (contextProxy) {
|
||||
const codebaseIndexConfig = contextProxy.getGlobalState("codebaseIndexConfig")
|
||||
const warpGrepApiKey = contextProxy.getSecret("warpGrepApiKey")
|
||||
|
||||
if (codebaseIndexConfig?.warpGrepEnabled && warpGrepApiKey) {
|
||||
const result = await executeWarpGrepSearch(
|
||||
workspacePath,
|
||||
query,
|
||||
warpGrepApiKey,
|
||||
task.rooIgnoreController,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
pushToolResult(`Query: ${query}\n\n${result.content}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Fall through to CodeIndexManager if available
|
||||
}
|
||||
}
|
||||
|
||||
const context = provider?.context
|
||||
if (!context) {
|
||||
throw new Error("Extension context is not available.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2202,6 +2202,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults,
|
||||
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
|
||||
codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider,
|
||||
warpGrepEnabled: settings.warpGrepEnabled,
|
||||
}
|
||||
|
||||
// Save global state first
|
||||
|
|
@ -2244,6 +2245,9 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
settings.codebaseIndexOpenRouterApiKey,
|
||||
)
|
||||
}
|
||||
if (settings.warpGrepApiKey !== undefined) {
|
||||
await provider.contextProxy.storeSecret("warpGrepApiKey", settings.warpGrepApiKey)
|
||||
}
|
||||
|
||||
// Send success response first - settings are saved regardless of validation
|
||||
await provider.postMessageToWebview({
|
||||
|
|
@ -2382,6 +2386,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
"codebaseIndexVercelAiGatewayApiKey",
|
||||
))
|
||||
const hasOpenRouterApiKey = !!(await provider.context.secrets.get("codebaseIndexOpenRouterApiKey"))
|
||||
const hasWarpGrepApiKey = !!(await provider.context.secrets.get("warpGrepApiKey"))
|
||||
|
||||
provider.postMessageToWebview({
|
||||
type: "codeIndexSecretStatus",
|
||||
|
|
@ -2393,6 +2398,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
|
|||
hasMistralApiKey,
|
||||
hasVercelAiGatewayApiKey,
|
||||
hasOpenRouterApiKey,
|
||||
hasWarpGrepApiKey,
|
||||
},
|
||||
})
|
||||
break
|
||||
|
|
|
|||
447
src/services/warpgrep/index.ts
Normal file
447
src/services/warpgrep/index.ts
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
import * as childProcess from "child_process"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
|
||||
import { getBinPath } from "../ripgrep"
|
||||
|
||||
const MAX_REPO_ENTRIES = 500
|
||||
const MAX_REPO_DEPTH = 4
|
||||
const MAX_READ_CHARS = 20_000
|
||||
const MAX_RG_OUTPUT_CHARS = 20_000
|
||||
const MAX_TOOL_CALLS_PER_TURN = 8
|
||||
const MAX_TURNS = 4
|
||||
const WARP_GREP_MODEL = "morph-warp-grep-v2"
|
||||
const IGNORED_DIRECTORIES = new Set([".git", ".next", ".turbo", "build", "coverage", "dist", "node_modules", "out"])
|
||||
|
||||
export interface WarpGrepToolCall {
|
||||
function: string
|
||||
parameters: Record<string, string>
|
||||
}
|
||||
|
||||
export interface WarpGrepSearchResult {
|
||||
success: boolean
|
||||
content: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
function ensureWorkspacePath(cwd: string, targetPath: string): { absolutePath: string; relativePath: string } {
|
||||
const absolutePath = path.resolve(cwd, targetPath)
|
||||
const relativePath = path.relative(cwd, absolutePath)
|
||||
|
||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
throw new Error(`Path is outside the workspace: ${targetPath}`)
|
||||
}
|
||||
|
||||
return { absolutePath, relativePath: relativePath || "." }
|
||||
}
|
||||
|
||||
function truncateOutput(content: string, maxChars: number): string {
|
||||
if (content.length <= maxChars) {
|
||||
return content
|
||||
}
|
||||
|
||||
return `${content.slice(0, maxChars)}\n... [truncated]`
|
||||
}
|
||||
|
||||
function escapeXml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'")
|
||||
}
|
||||
|
||||
function extractMessageContent(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return part
|
||||
}
|
||||
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
||||
return part.text
|
||||
}
|
||||
return ""
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function parseLineRanges(lineRange?: string): Array<{ start: number; end: number }> {
|
||||
if (!lineRange?.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
return lineRange
|
||||
.split(",")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean)
|
||||
.map((segment) => {
|
||||
const [rawStart, rawEnd] = segment.split("-").map((part) => part.trim())
|
||||
const start = Number.parseInt(rawStart, 10)
|
||||
const end = Number.parseInt(rawEnd || rawStart, 10)
|
||||
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0 || end < start) {
|
||||
throw new Error(`Invalid line range: ${segment}`)
|
||||
}
|
||||
|
||||
return { start, end }
|
||||
})
|
||||
}
|
||||
|
||||
export async function buildRepoStructure(
|
||||
cwd: string,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
maxDepth: number = MAX_REPO_DEPTH,
|
||||
maxEntries: number = MAX_REPO_ENTRIES,
|
||||
): Promise<string> {
|
||||
const lines = [path.basename(cwd) || "."]
|
||||
let entryCount = 0
|
||||
let wasTruncated = false
|
||||
|
||||
const walk = async (directoryPath: string, depth: number) => {
|
||||
if (depth >= maxDepth || wasTruncated) {
|
||||
return
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(directoryPath, { withFileTypes: true })
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entryCount >= maxEntries) {
|
||||
wasTruncated = true
|
||||
return
|
||||
}
|
||||
|
||||
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const absoluteEntryPath = path.join(directoryPath, entry.name)
|
||||
const relativePath = path.relative(cwd, absoluteEntryPath) || entry.name
|
||||
if (rooIgnoreController && !rooIgnoreController.validateAccess(relativePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
lines.push(`${" ".repeat(depth + 1)}- ${relativePath}${entry.isDirectory() ? "/" : ""}`)
|
||||
entryCount += 1
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(absoluteEntryPath, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(cwd, 0)
|
||||
|
||||
if (wasTruncated) {
|
||||
lines.push(" ... [repo structure truncated]")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export function parseToolCalls(content: string): WarpGrepToolCall[] {
|
||||
const toolCalls: WarpGrepToolCall[] = []
|
||||
const toolCallRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g
|
||||
|
||||
for (const match of content.matchAll(toolCallRegex)) {
|
||||
const rawBody = match[1]
|
||||
const directFunctionMatch = rawBody.match(/<function(?:=| name=)(["']?)([\w-]+)\1>/)
|
||||
const blockFunctionMatch = rawBody.match(/<function>([\w-]+)<\/function>/)
|
||||
const functionName = directFunctionMatch?.[2] || blockFunctionMatch?.[1]
|
||||
|
||||
if (!functionName) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parameters: Record<string, string> = {}
|
||||
const tagRegex = /<([a-zA-Z_][\w-]*)>([\s\S]*?)<\/\1>/g
|
||||
for (const tagMatch of rawBody.matchAll(tagRegex)) {
|
||||
const [, key, value] = tagMatch
|
||||
if (key !== "function") {
|
||||
parameters[key] = value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
function: functionName,
|
||||
parameters,
|
||||
})
|
||||
}
|
||||
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
export async function executeRipgrep(
|
||||
cwd: string,
|
||||
pattern: string,
|
||||
searchPath: string = ".",
|
||||
glob?: string,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<string> {
|
||||
const { absolutePath, relativePath } = ensureWorkspacePath(cwd, searchPath)
|
||||
|
||||
if (rooIgnoreController && relativePath !== "." && !rooIgnoreController.validateAccess(relativePath)) {
|
||||
throw new Error(`Path is blocked by .rooignore: ${searchPath}`)
|
||||
}
|
||||
|
||||
const rgPath = await getBinPath(vscode.env.appRoot)
|
||||
if (!rgPath) {
|
||||
throw new Error("Could not find ripgrep binary")
|
||||
}
|
||||
|
||||
const args = ["--line-number", "--with-filename", "--color", "never", "--no-heading"]
|
||||
if (glob) {
|
||||
args.push("--glob", glob)
|
||||
}
|
||||
args.push(pattern, absolutePath)
|
||||
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const rg = childProcess.spawn(rgPath, args, { cwd })
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
rg.stdout.on("data", (chunk) => {
|
||||
if (stdout.length < MAX_RG_OUTPUT_CHARS) {
|
||||
stdout += chunk.toString()
|
||||
}
|
||||
})
|
||||
|
||||
rg.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString()
|
||||
})
|
||||
|
||||
rg.on("error", reject)
|
||||
rg.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout)
|
||||
return
|
||||
}
|
||||
if (code === 1) {
|
||||
resolve("")
|
||||
return
|
||||
}
|
||||
reject(new Error(stderr || `ripgrep exited with code ${code}`))
|
||||
})
|
||||
})
|
||||
|
||||
return result.trim() ? truncateOutput(result.trim(), MAX_RG_OUTPUT_CHARS) : "No matches found."
|
||||
}
|
||||
|
||||
export async function readFile(
|
||||
cwd: string,
|
||||
filePath: string,
|
||||
lineRange?: string,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<string> {
|
||||
const { absolutePath, relativePath } = ensureWorkspacePath(cwd, filePath)
|
||||
|
||||
if (rooIgnoreController && !rooIgnoreController.validateAccess(relativePath)) {
|
||||
throw new Error(`Path is blocked by .rooignore: ${filePath}`)
|
||||
}
|
||||
|
||||
const fileContents = await fs.readFile(absolutePath, "utf8")
|
||||
if (!lineRange?.trim()) {
|
||||
return truncateOutput(fileContents, MAX_READ_CHARS)
|
||||
}
|
||||
|
||||
const lines = fileContents.split("\n")
|
||||
const segments = parseLineRanges(lineRange).map(({ start, end }) => {
|
||||
const selected = lines.slice(start - 1, end).join("\n")
|
||||
return `Lines ${start}-${end}:\n${selected}`
|
||||
})
|
||||
|
||||
return truncateOutput(segments.join("\n\n"), MAX_READ_CHARS)
|
||||
}
|
||||
|
||||
export async function listDirectory(
|
||||
cwd: string,
|
||||
dirPath: string = ".",
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<string> {
|
||||
const { absolutePath, relativePath } = ensureWorkspacePath(cwd, dirPath)
|
||||
|
||||
if (rooIgnoreController && relativePath !== "." && !rooIgnoreController.validateAccess(relativePath)) {
|
||||
throw new Error(`Path is blocked by .rooignore: ${dirPath}`)
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(absolutePath, { withFileTypes: true })
|
||||
const visibleEntries = entries
|
||||
.filter((entry) => {
|
||||
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
||||
return false
|
||||
}
|
||||
if (!rooIgnoreController) {
|
||||
return true
|
||||
}
|
||||
const entryRelativePath = path.relative(cwd, path.join(absolutePath, entry.name))
|
||||
return rooIgnoreController.validateAccess(entryRelativePath)
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((entry) => `${entry.isDirectory() ? "dir" : "file"} ${entry.name}`)
|
||||
|
||||
return visibleEntries.length > 0 ? visibleEntries.join("\n") : "(empty directory)"
|
||||
}
|
||||
|
||||
async function executeToolCall(
|
||||
cwd: string,
|
||||
toolCall: WarpGrepToolCall,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<string> {
|
||||
switch (toolCall.function) {
|
||||
case "ripgrep":
|
||||
return executeRipgrep(
|
||||
cwd,
|
||||
toolCall.parameters.pattern ?? "",
|
||||
toolCall.parameters.path ?? ".",
|
||||
toolCall.parameters.glob,
|
||||
rooIgnoreController,
|
||||
)
|
||||
case "read":
|
||||
return readFile(cwd, toolCall.parameters.path ?? "", toolCall.parameters.lines, rooIgnoreController)
|
||||
case "list_directory":
|
||||
return listDirectory(cwd, toolCall.parameters.path ?? ".", rooIgnoreController)
|
||||
default:
|
||||
throw new Error(`Unsupported WarpGrep tool: ${toolCall.function}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleFinish(
|
||||
cwd: string,
|
||||
filesParam: string,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<string> {
|
||||
const fileSpecs = filesParam
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (fileSpecs.length === 0) {
|
||||
return "WarpGrep finished without returning any file ranges."
|
||||
}
|
||||
|
||||
const sections = await Promise.all(
|
||||
fileSpecs.map(async (fileSpec) => {
|
||||
const separatorIndex = fileSpec.indexOf(":")
|
||||
const filePath = separatorIndex === -1 ? fileSpec : fileSpec.slice(0, separatorIndex)
|
||||
const lineRange = separatorIndex === -1 ? undefined : fileSpec.slice(separatorIndex + 1)
|
||||
const content = await readFile(cwd, filePath, lineRange, rooIgnoreController)
|
||||
return `File: ${filePath}\n${content}`
|
||||
}),
|
||||
)
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
|
||||
function buildInitialPrompt(repoStructure: string, query: string): string {
|
||||
return [
|
||||
"You are WarpGrep, a codebase search subagent.",
|
||||
"Use the available XML tools to locate the most relevant files and return finish with precise file:line-range specs.",
|
||||
"<repo_structure>",
|
||||
repoStructure,
|
||||
"</repo_structure>",
|
||||
"<search_string>",
|
||||
query,
|
||||
"</search_string>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
function buildToolResponseMessage(
|
||||
toolResponses: Array<{ toolCall: WarpGrepToolCall; result: string }>,
|
||||
turn: number,
|
||||
): string {
|
||||
return [
|
||||
...toolResponses.map(
|
||||
({ toolCall, result }) =>
|
||||
`<tool_response><function>${toolCall.function}</function><result>${escapeXml(result)}</result></tool_response>`,
|
||||
),
|
||||
`<turn_context>Turn ${turn + 1} of ${MAX_TURNS}</turn_context>`,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export async function executeWarpGrepSearch(
|
||||
cwd: string,
|
||||
query: string,
|
||||
apiKey: string,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): Promise<WarpGrepSearchResult> {
|
||||
try {
|
||||
const repoStructure = await buildRepoStructure(cwd, rooIgnoreController)
|
||||
const messages: Array<{ role: "user" | "assistant"; content: string }> = [
|
||||
{ role: "user", content: buildInitialPrompt(repoStructure, query) },
|
||||
]
|
||||
|
||||
for (let turn = 0; turn < MAX_TURNS; turn += 1) {
|
||||
const response = await fetch("https://api.morphllm.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: WARP_GREP_MODEL,
|
||||
temperature: 0,
|
||||
max_tokens: 2048,
|
||||
messages,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`WarpGrep request failed (${response.status}): ${errorText}`)
|
||||
}
|
||||
|
||||
const payload = await response.json()
|
||||
const content = extractMessageContent(payload?.choices?.[0]?.message?.content).trim()
|
||||
if (!content) {
|
||||
throw new Error("WarpGrep returned an empty response")
|
||||
}
|
||||
|
||||
const toolCalls = parseToolCalls(content).slice(0, MAX_TOOL_CALLS_PER_TURN)
|
||||
if (toolCalls.length === 0) {
|
||||
return { success: true, content }
|
||||
}
|
||||
|
||||
const toolResponses: Array<{ toolCall: WarpGrepToolCall; result: string }> = []
|
||||
for (const toolCall of toolCalls) {
|
||||
if (toolCall.function === "finish") {
|
||||
const filesParam = toolCall.parameters.files ?? ""
|
||||
return {
|
||||
success: true,
|
||||
content: await handleFinish(cwd, filesParam, rooIgnoreController),
|
||||
}
|
||||
}
|
||||
|
||||
const result = await executeToolCall(cwd, toolCall, rooIgnoreController)
|
||||
toolResponses.push({ toolCall, result })
|
||||
}
|
||||
|
||||
messages.push({ role: "assistant", content })
|
||||
messages.push({ role: "user", content: buildToolResponseMessage(toolResponses, turn) })
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
content: "",
|
||||
error: `WarpGrep did not finish within ${MAX_TURNS} turns.`,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
success: false,
|
||||
content: "",
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,9 @@ interface LocalCodeIndexSettings {
|
|||
codebaseIndexVercelAiGatewayApiKey?: string
|
||||
codebaseIndexOpenRouterApiKey?: string
|
||||
codebaseIndexOpenRouterSpecificProvider?: string
|
||||
|
||||
warpGrepEnabled: boolean
|
||||
warpGrepApiKey?: string
|
||||
}
|
||||
|
||||
// Validation schema for codebase index settings
|
||||
|
|
@ -225,6 +228,8 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
codebaseIndexVercelAiGatewayApiKey: "",
|
||||
codebaseIndexOpenRouterApiKey: "",
|
||||
codebaseIndexOpenRouterSpecificProvider: "",
|
||||
warpGrepEnabled: false,
|
||||
warpGrepApiKey: "",
|
||||
})
|
||||
|
||||
// Initial settings state - stores the settings when popover opens
|
||||
|
|
@ -265,6 +270,8 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
codebaseIndexOpenRouterApiKey: "",
|
||||
codebaseIndexOpenRouterSpecificProvider:
|
||||
codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider || "",
|
||||
warpGrepEnabled: codebaseIndexConfig.warpGrepEnabled ?? false,
|
||||
warpGrepApiKey: "",
|
||||
}
|
||||
setInitialSettings(settings)
|
||||
setCurrentSettings(settings)
|
||||
|
|
@ -389,6 +396,9 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
? SECRET_PLACEHOLDER
|
||||
: ""
|
||||
}
|
||||
if (!prev.warpGrepApiKey || prev.warpGrepApiKey === SECRET_PLACEHOLDER) {
|
||||
updated.warpGrepApiKey = secretStatus.hasWarpGrepApiKey ? SECRET_PLACEHOLDER : ""
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
|
@ -553,8 +563,9 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
settingsToSave[key] = value
|
||||
}
|
||||
|
||||
// Always include codebaseIndexEnabled to ensure it's persisted
|
||||
// Always include toggle states to ensure they're persisted
|
||||
settingsToSave.codebaseIndexEnabled = currentSettings.codebaseIndexEnabled
|
||||
settingsToSave.warpGrepEnabled = currentSettings.warpGrepEnabled
|
||||
|
||||
// Save settings to backend
|
||||
vscode.postMessage({
|
||||
|
|
@ -1590,6 +1601,40 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* WarpGrep Section */}
|
||||
<div className="mt-4 pt-4 border-t border-vscode-dropdown-border">
|
||||
<h4 className="text-sm font-semibold mb-2">WarpGrep</h4>
|
||||
<p className="text-xs text-vscode-descriptionForeground mb-3">
|
||||
Alternative codebase search backend that uses an agentic loop with ripgrep and file
|
||||
reads to find relevant code spans.
|
||||
</p>
|
||||
<div className="mb-3">
|
||||
<VSCodeCheckbox
|
||||
checked={currentSettings.warpGrepEnabled}
|
||||
onChange={(e: any) => updateSetting("warpGrepEnabled", e.target.checked)}>
|
||||
<span className="font-medium">Enable WarpGrep</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
{currentSettings.warpGrepEnabled && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">API Key</label>
|
||||
<VSCodeTextField
|
||||
type="password"
|
||||
value={currentSettings.warpGrepApiKey || ""}
|
||||
onInput={(e: any) => updateSetting("warpGrepApiKey", e.target.value)}
|
||||
placeholder="Enter your Morph API key"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-vscode-descriptionForeground mt-1 mb-0">
|
||||
Get your API key at{" "}
|
||||
<VSCodeLink href="https://morphllm.com" style={{ display: "inline" }}>
|
||||
morphllm.com
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auto-enable default */}
|
||||
{currentSettings.codebaseIndexEnabled && (
|
||||
<div className="flex items-center gap-2 pt-4 pb-1">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue