Unverified 提交 50ca2fa1 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Code search tool (#2167)

<!-- CURSOR_SUMMARY --> > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit 3cc11d352d12a0f51da474e5e3a281c128f2a9c4. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> ## Summary by cubic Adds a semantic code search tool to the local agent with an interactive UI card, so the agent can find relevant files by meaning, not just text. Includes E2E coverage for the full flow. - New Features - code_search tool: gathers project files, calls Dyad Engine /tools/code-search, and returns relevant file paths. - Streams a dyad-code-search tag while running; shows pending state in the UI. Requires a Dyad Pro API key. - DyadCodeSearch UI: collapsible card with query preview, results, and loading indicator. - Markdown parser now passes query and state; tool registered in tool_definitions. - Tests - Added E2E spec, fixture, and snapshot for local-agent code search. - Fake LLM server includes a mock /engine/v1/tools/code-search endpoint. <sup>Written for commit 3cc11d352d12a0f51da474e5e3a281c128f2a9c4. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 5bd8a703
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
export const fixture: LocalAgentFixture = {
description: "Search for relevant files in codebase using code_search tool",
turns: [
{
text: "I'll search for files related to React components in the codebase.",
toolCalls: [
{
name: "code_search",
args: {
query: "React component rendering",
},
},
],
},
{
text: "I found the relevant files! The main React component is in src/App.tsx which handles the app rendering.",
},
],
};
import { testSkipIfWindows } from "./helpers/test_helper";
/**
* E2E tests for the code_search agent tool
* Tests semantic code search in local-agent mode
*/
testSkipIfWindows("local-agent - code search", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.selectLocalAgentMode();
await po.sendPrompt("tc=local-agent/code-search");
await po.snapshotMessages();
});
...@@ -225,6 +225,27 @@ ...@@ -225,6 +225,27 @@
} }
} }
}, },
{
"type": "function",
"function": {
"name": "code_search",
"description": "Search the codebase semantically to find files relevant to a query. Use this tool when you need to discover which files contain code related to a specific concept, feature, or functionality. Returns a list of file paths that are most relevant to the search query.\n\n### When to Use This Tool\n\n- Explore unfamiliar codebases\n- Ask \"how / where / what\" questions to understand behavior\n- Find code by meaning rather than exact text\n\n### When NOT to Use\n\nSkip this tool for:\n1. Exact text matches (use `grep`)\n2. Reading known files (use `read_file`)\n3. Simple symbol lookups (use `grep`)\n",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query to find relevant files"
}
},
"required": [
"query"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{ {
"type": "function", "type": "function",
"function": { "function": {
......
- paragraph: /Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
- img
- text: file1.txt
- button "Edit":
- img
- img
- text: file1.txt
- paragraph: More EOM
- button:
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- paragraph: tc=local-agent/code-search
- paragraph: I'll search for files related to React components in the codebase.
- 'button "Code Search React component rendering Query: React component rendering Results: - .gitignore - index.html - package.json"':
- img
- img
- paragraph: I found the relevant files! The main React component is in src/App.tsx which handles the app rendering.
- button:
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
import type React from "react"; import type React from "react";
import type { ReactNode } from "react"; import { useState, type ReactNode } from "react";
import { FileCode } from "lucide-react"; import { ChevronDown, ChevronUp, FileCode, Loader } from "lucide-react";
import { CustomTagState } from "./stateTypes";
interface DyadCodeSearchProps { interface DyadCodeSearchProps {
children?: ReactNode; children?: ReactNode;
node?: any; node?: { properties?: { query?: string; state?: CustomTagState } };
query?: string;
} }
export const DyadCodeSearch: React.FC<DyadCodeSearchProps> = ({ export const DyadCodeSearch: React.FC<DyadCodeSearchProps> = ({
children, children,
node: _node, node,
query: queryProp,
}) => { }) => {
const query = queryProp || (typeof children === "string" ? children : ""); const [isExpanded, setIsExpanded] = useState(false);
const query =
node?.properties?.query || (typeof children === "string" ? children : "");
const state = node?.properties?.state as CustomTagState;
const inProgress = state === "pending";
return ( return (
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border my-2"> <div
className={`bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
inProgress ? "border-purple-500" : "border-border"
}`}
onClick={() => setIsExpanded(!isExpanded)}
role="button"
aria-expanded={isExpanded}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setIsExpanded(!isExpanded);
}
}}
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileCode size={16} className="text-purple-600" /> <FileCode size={16} className="text-purple-600" />
<div className="text-xs text-purple-600 font-medium">Code Search</div> <div className="text-xs text-purple-600 font-medium">Code Search</div>
{inProgress && (
<div className="flex items-center text-purple-600 text-xs">
<Loader size={14} className="mr-1 animate-spin" />
<span>Searching...</span>
</div> </div>
)}
</div>
<div className="p-1 text-gray-500">
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
</div>
{/* Collapsed preview - show query */}
<div
className="text-sm italic text-gray-600 dark:text-gray-300 mt-2 overflow-hidden transition-all duration-300 ease-in-out"
style={{
maxHeight: isExpanded ? "0px" : "3em",
opacity: isExpanded ? 0 : 1,
}}
>
{query}
</div>
{/* Expanded content */}
<div
className="overflow-hidden transition-all duration-300 ease-in-out"
style={{
maxHeight: isExpanded ? "none" : "0px",
opacity: isExpanded ? 1 : 0,
marginTop: isExpanded ? "0.5rem" : "0",
}}
>
<div className="text-sm text-gray-600 dark:text-gray-300 space-y-2">
{query && (
<div>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">
Query:
</span>
<div className="italic mt-0.5">{query}</div>
</div>
)}
{children && (
<div>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">
Results:
</span>
<div className="mt-0.5 whitespace-pre-wrap font-mono text-xs">
{children}
</div>
</div>
)}
</div> </div>
<div className="text-sm italic text-gray-600 dark:text-gray-300 mt-2">
{query || children}
</div> </div>
</div> </div>
); );
......
...@@ -374,7 +374,10 @@ function renderCustomTag( ...@@ -374,7 +374,10 @@ function renderCustomTag(
return ( return (
<DyadCodeSearch <DyadCodeSearch
node={{ node={{
properties: {}, properties: {
query: attributes.query || "",
state: getState({ isStreaming, inProgress }),
},
}} }}
> >
{content} {content}
......
...@@ -25,6 +25,7 @@ import { webCrawlTool } from "./tools/web_crawl"; ...@@ -25,6 +25,7 @@ import { webCrawlTool } from "./tools/web_crawl";
import { updateTodosTool } from "./tools/update_todos"; import { updateTodosTool } from "./tools/update_todos";
import { runTypeChecksTool } from "./tools/run_type_checks"; import { runTypeChecksTool } from "./tools/run_type_checks";
import { grepTool } from "./tools/grep"; import { grepTool } from "./tools/grep";
import { codeSearchTool } from "./tools/code_search";
import type { LanguageModelV3ToolResultOutput } from "@ai-sdk/provider"; import type { LanguageModelV3ToolResultOutput } from "@ai-sdk/provider";
import { import {
escapeXmlAttr, escapeXmlAttr,
...@@ -48,6 +49,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [ ...@@ -48,6 +49,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [
readFileTool, readFileTool,
listFilesTool, listFilesTool,
grepTool, grepTool,
codeSearchTool,
getSupabaseProjectInfoTool, getSupabaseProjectInfoTool,
getSupabaseTableSchemaTool, getSupabaseTableSchemaTool,
setChatSummaryTool, setChatSummaryTool,
......
import { z } from "zod";
import log from "electron-log";
import {
ToolDefinition,
AgentContext,
escapeXmlAttr,
escapeXmlContent,
} from "./types";
import { extractCodebase } from "../../../../../../utils/codebase";
import { readSettings } from "@/main/settings";
const logger = log.scope("code_search");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const codeSearchSchema = z.object({
query: z.string().describe("Search query to find relevant files"),
});
const FileContextSchema = z.object({
path: z.string(),
content: z.string(),
});
const codeSearchResponseSchema = z.object({
relevantFiles: z.array(z.string()).describe("Paths of relevant files"),
});
async function callCodeSearch(
params: {
query: string;
filesContext: z.infer<typeof FileContextSchema>[];
},
ctx: AgentContext,
): Promise<string[]> {
const settings = readSettings();
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required for code_search tool");
}
// Stream initial state to UI
ctx.onXmlStream(`<dyad-code-search query="${escapeXmlAttr(params.query)}">`);
const response = await fetch(`${DYAD_ENGINE_URL}/tools/code-search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
query: params.query,
filesContext: params.filesContext,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Code search failed: ${response.status} ${response.statusText} - ${errorText}`,
);
}
const data = codeSearchResponseSchema.parse(await response.json());
return data.relevantFiles;
}
const DESCRIPTION = `Search the codebase semantically to find files relevant to a query. Use this tool when you need to discover which files contain code related to a specific concept, feature, or functionality. Returns a list of file paths that are most relevant to the search query.
### When to Use This Tool
- Explore unfamiliar codebases
- Ask "how / where / what" questions to understand behavior
- Find code by meaning rather than exact text
### When NOT to Use
Skip this tool for:
1. Exact text matches (use \`grep\`)
2. Reading known files (use \`read_file\`)
3. Simple symbol lookups (use \`grep\`)
`;
export const codeSearchTool: ToolDefinition<z.infer<typeof codeSearchSchema>> =
{
name: "code_search",
description: DESCRIPTION,
inputSchema: codeSearchSchema,
defaultConsent: "always",
getConsentPreview: (args) => `Search for "${args.query}"`,
buildXml: (args, isComplete) => {
if (!args.query) return undefined;
if (isComplete) return undefined;
return `<dyad-code-search query="${escapeXmlAttr(args.query)}">Searching...`;
},
execute: async (args, ctx: AgentContext) => {
logger.log(`Executing code search: ${args.query}`);
// Gather all files from the project
const { files } = await extractCodebase({
appPath: ctx.appPath,
chatContext: {
contextPaths: [],
smartContextAutoIncludes: [],
excludePaths: [],
},
});
// Map files to FileContext format
const filesContext = files.map((file) => ({
path: file.path,
content: file.content,
}));
logger.log(
`Searching ${filesContext.length} files for query: "${args.query}"`,
);
// Call the code-search endpoint
const relevantFiles = await callCodeSearch(
{
query: args.query,
filesContext,
},
ctx,
);
// Format results
const resultText =
relevantFiles.length === 0
? "No relevant files found."
: relevantFiles.map((f) => ` - ${f}`).join("\n");
// Write final result to UI and DB with dyad-code-search wrapper
ctx.onXmlComplete(
`<dyad-code-search query="${escapeXmlAttr(args.query)}">${escapeXmlContent(resultText)}</dyad-code-search>`,
);
logger.log(`Code search completed for query: ${args.query}`);
if (relevantFiles.length === 0) {
return "No relevant files found for the given query.";
}
return `Found ${relevantFiles.length} relevant file(s):\n${resultText}`;
},
};
...@@ -202,6 +202,27 @@ app.post("/engine/v1/tools/turbo-file-edit", (req, res) => { ...@@ -202,6 +202,27 @@ app.post("/engine/v1/tools/turbo-file-edit", (req, res) => {
} }
}); });
// Dyad Engine code-search endpoint for code_search tool
app.post("/engine/v1/tools/code-search", (req, res) => {
const { query, filesContext } = req.body;
console.log(
`* code-search: "${query}" - searching ${filesContext?.length || 0} files`,
);
try {
// Return mock relevant files based on the files provided
// For testing, return the first few files that exist in the context
const relevantFiles = (filesContext || [])
.slice(0, 3)
.map((f: { path: string }) => f.path);
res.json({ relevantFiles });
} catch (error) {
console.error(`* code-search error:`, error);
res.status(400).json({ error: String(error) });
}
});
// Start the server // Start the server
const server = createServer(app); const server = createServer(app);
server.listen(PORT, () => { server.listen(PORT, () => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论