diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 61009ba3011..94a298b81a1 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -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 @@ -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 diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 288f6c2118c..24ba367bc86 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -275,6 +275,7 @@ export const SECRET_STATE_KEYS = [ "codebaseIndexMistralApiKey", "codebaseIndexVercelAiGatewayApiKey", "codebaseIndexOpenRouterApiKey", + "warpGrepApiKey", "sambaNovaApiKey", "zaiApiKey", "fireworksApiKey", diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe49..b2cce0539a7 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -678,6 +678,10 @@ export interface WebviewMessage { codebaseIndexMistralApiKey?: string codebaseIndexVercelAiGatewayApiKey?: string codebaseIndexOpenRouterApiKey?: string + + // WarpGrep specific fields + warpGrepEnabled?: boolean + warpGrepApiKey?: string } updatedSettings?: RooCodeSettings /** Task configuration applied via `createTask()` when starting a cloud task. */ diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.warpgrep.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.warpgrep.spec.ts new file mode 100644 index 00000000000..bd1fcd4b612 --- /dev/null +++ b/src/core/prompts/tools/__tests__/filter-tools-for-mode.warpgrep.spec.ts @@ -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) + }) +}) diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index fdd41e7e330..eb83b66ed55 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -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") { diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index c32d8f6f9b2..643010c7b9d 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -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, } diff --git a/src/core/tools/CodebaseSearchTool.ts b/src/core/tools/CodebaseSearchTool.ts index f0d906fabd8..c9b797460f2 100644 --- a/src/core/tools/CodebaseSearchTool.ts +++ b/src/core/tools/CodebaseSearchTool.ts @@ -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.") } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d27fd6bec09..ffe4bad50b0 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2517,6 +2517,7 @@ export const webviewMessageHandler = async ( codebaseIndexSearchMaxResults: settings.codebaseIndexSearchMaxResults, codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore, codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider, + warpGrepEnabled: settings.warpGrepEnabled, } // Save global state first @@ -2559,6 +2560,9 @@ export const webviewMessageHandler = async ( 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({ @@ -2697,6 +2701,7 @@ export const webviewMessageHandler = async ( "codebaseIndexVercelAiGatewayApiKey", )) const hasOpenRouterApiKey = !!(await provider.context.secrets.get("codebaseIndexOpenRouterApiKey")) + const hasWarpGrepApiKey = !!(await provider.context.secrets.get("warpGrepApiKey")) provider.postMessageToWebview({ type: "codeIndexSecretStatus", @@ -2708,6 +2713,7 @@ export const webviewMessageHandler = async ( hasMistralApiKey, hasVercelAiGatewayApiKey, hasOpenRouterApiKey, + hasWarpGrepApiKey, }, }) break diff --git a/src/services/warpgrep/index.ts b/src/services/warpgrep/index.ts new file mode 100644 index 00000000000..0ce84156064 --- /dev/null +++ b/src/services/warpgrep/index.ts @@ -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 +} + +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 { + 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 = /([\s\S]*?)<\/tool_call>/g + + for (const match of content.matchAll(toolCallRegex)) { + const rawBody = match[1] + const directFunctionMatch = rawBody.match(//) + const blockFunctionMatch = rawBody.match(/([\w-]+)<\/function>/) + const functionName = directFunctionMatch?.[2] || blockFunctionMatch?.[1] + + if (!functionName) { + continue + } + + const parameters: Record = {} + 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 { + 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((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 { + 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 { + 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 { + 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 { + 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.", + "", + repoStructure, + "", + "", + query, + "", + ].join("\n") +} + +function buildToolResponseMessage( + toolResponses: Array<{ toolCall: WarpGrepToolCall; result: string }>, + turn: number, +): string { + return [ + ...toolResponses.map( + ({ toolCall, result }) => + `${toolCall.function}${escapeXml(result)}`, + ), + `Turn ${turn + 1} of ${MAX_TURNS}`, + ].join("\n") +} + +export async function executeWarpGrepSearch( + cwd: string, + query: string, + apiKey: string, + rooIgnoreController?: RooIgnoreController, +): Promise { + 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, + } + } +} diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 763c243ec1e..b8845ed7aa5 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -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 = ({ codebaseIndexVercelAiGatewayApiKey: "", codebaseIndexOpenRouterApiKey: "", codebaseIndexOpenRouterSpecificProvider: "", + warpGrepEnabled: false, + warpGrepApiKey: "", }) // Initial settings state - stores the settings when popover opens @@ -265,6 +270,8 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenRouterApiKey: "", codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider || "", + warpGrepEnabled: codebaseIndexConfig.warpGrepEnabled ?? false, + warpGrepApiKey: "", } setInitialSettings(settings) setCurrentSettings(settings) @@ -389,6 +396,9 @@ export const CodeIndexPopover: React.FC = ({ ? 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 = ({ 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 = ({ )} + {/* WarpGrep Section */} +
+

WarpGrep

+

+ Alternative codebase search backend that uses an agentic loop with ripgrep and file + reads to find relevant code spans. +

+
+ updateSetting("warpGrepEnabled", e.target.checked)}> + Enable WarpGrep + +
+ {currentSettings.warpGrepEnabled && ( +
+ + updateSetting("warpGrepApiKey", e.target.value)} + placeholder="Enter your Morph API key" + className="w-full" + /> +

+ Get your API key at{" "} + + morphllm.com + +

+
+ )} +
+ {/* Auto-enable default */} {currentSettings.codebaseIndexEnabled && (