Unverified 提交 b1fa3ad6 authored 作者: keppo-bot[bot]'s avatar keppo-bot[bot] 提交者: GitHub

Allow local agent to inspect ignored files (#3256)

## Summary - Add include_ignored to local-agent grep and list_files so ignored and hidden paths can be inspected only when requested. - Remove include_hidden from list_files, add a root recursive ignored-listing guard, cap list output at 1000 paths, and sort directories before files. - Update local-agent tests and snapshots, including a less brittle read_logs E2E assertion. ## Test plan - PYTHON=/usr/bin/python3 npm run build - PLAYWRIGHT_HTML_OPEN=never npm run e2e -- e2e-tests/local_agent_search_replace.spec.ts e2e-tests/local_agent_step_limit.spec.ts e2e-tests/local_agent_consent.spec.ts e2e-tests/local_agent_code_search.spec.ts e2e-tests/local_agent_advanced.spec.ts e2e-tests/local_agent_file_upload.spec.ts e2e-tests/local_agent_basic.spec.ts e2e-tests/local_agent_grep.spec.ts e2e-tests/local_agent_auto.spec.ts e2e-tests/local_agent_list_files.spec.ts e2e-tests/local_agent_ask.spec.ts e2e-tests/local_agent_todo_followup.spec.ts e2e-tests/local_agent_summarize.spec.ts e2e-tests/local_agent_read_logs.spec.ts e2e-tests/local_agent_generate_image.spec.ts e2e-tests/local_agent_connection_retry.spec.ts e2e-tests/local_agent_persistent_todos.spec.ts e2e-tests/local_agent_web_fetch.spec.ts e2e-tests/local_agent_run_type_checks.spec.ts - npm run fmt && npm run lint:fix && npm run ts - npm test <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3256" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarWill Chen <7344640+wwwillchen@users.noreply.github.com>
上级 968c654c
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
export const fixture: LocalAgentFixture = {
description: "Search ignored files using grep",
turns: [
{
text: "I'll search the ignored dependency folder for the requested symbol.",
toolCalls: [
{
name: "grep",
args: {
query: "ignoredNeedle",
include_pattern: "node_modules/ignored-pkg/**",
include_ignored: true,
},
},
],
},
{
text: "I found the symbol in the ignored dependency folder.",
},
],
};
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes"; import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
export const fixture: LocalAgentFixture = { export const fixture: LocalAgentFixture = {
description: "List files including hidden .dyad files", description: "List files including ignored .dyad files",
turns: [ turns: [
{ {
text: "I'll list all files including the hidden .dyad directory for you.", text: "I'll list all files including the ignored .dyad directory for you.",
toolCalls: [ toolCalls: [
{ {
name: "list_files", name: "list_files",
args: { args: {
directory: ".dyad",
recursive: true, recursive: true,
include_hidden: true, include_ignored: true,
}, },
}, },
], ],
}, },
{ {
text: "Here are all the files including the hidden .dyad files.", text: "Here are the ignored .dyad files.",
}, },
], ],
}; };
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import fs from "fs";
import path from "path";
import { testSkipIfWindows } from "./helpers/test_helper"; import { testSkipIfWindows } from "./helpers/test_helper";
/** /**
...@@ -19,3 +21,28 @@ testSkipIfWindows("local-agent - grep search", async ({ po }) => { ...@@ -19,3 +21,28 @@ testSkipIfWindows("local-agent - grep search", async ({ po }) => {
await expect(po.page.getByTestId("dyad-grep").first()).toMatchAriaSnapshot(); await expect(po.page.getByTestId("dyad-grep").first()).toMatchAriaSnapshot();
await expect(po.page.getByTestId("dyad-grep").nth(1)).toMatchAriaSnapshot(); await expect(po.page.getByTestId("dyad-grep").nth(1)).toMatchAriaSnapshot();
}); });
testSkipIfWindows(
"local-agent - grep searches ignored files when requested",
async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectLocalAgentMode();
const appPath = await po.appManagement.getCurrentAppPath();
const ignoredPackageDir = path.join(appPath, "node_modules", "ignored-pkg");
fs.mkdirSync(ignoredPackageDir, { recursive: true });
fs.writeFileSync(
path.join(ignoredPackageDir, "index.js"),
"export const ignoredNeedle = 'search ignored files';\n",
);
await po.sendPrompt("tc=local-agent/grep-include-ignored");
const grepCard = po.page.getByTestId("dyad-grep").first();
await expect(grepCard).toContainText('"ignoredNeedle"');
await grepCard.click();
await expect(grepCard).toContainText("node_modules/ignored-pkg/index.js");
await expect(grepCard).toContainText("ignoredNeedle");
},
);
...@@ -21,13 +21,16 @@ testSkipIfWindows("local-agent - list_files", async ({ po }) => { ...@@ -21,13 +21,16 @@ testSkipIfWindows("local-agent - list_files", async ({ po }) => {
await expect(listFiles2).toMatchAriaSnapshot(); await expect(listFiles2).toMatchAriaSnapshot();
}); });
testSkipIfWindows("local-agent - list_files include_hidden", async ({ po }) => { testSkipIfWindows(
await po.setUpDyadPro({ localAgent: true }); "local-agent - list_files include_ignored",
await po.importApp("minimal-with-dyad"); async ({ po }) => {
await po.chatActions.selectLocalAgentMode(); await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal-with-dyad");
await po.chatActions.selectLocalAgentMode();
await po.sendPrompt("tc=local-agent/list-files-include-hidden"); await po.sendPrompt("tc=local-agent/list-files-include-ignored");
const listFiles = po.page.getByTestId("dyad-list-files").first(); const listFiles = po.page.getByTestId("dyad-list-files").first();
await listFiles.click(); await listFiles.click();
await expect(listFiles).toMatchAriaSnapshot(); await expect(listFiles).toMatchAriaSnapshot();
}); },
);
import { expect } from "@playwright/test";
import { testSkipIfWindows } from "./helpers/test_helper"; import { testSkipIfWindows } from "./helpers/test_helper";
/** /**
...@@ -18,5 +19,27 @@ testSkipIfWindows("local-agent - read logs with filters", async ({ po }) => { ...@@ -18,5 +19,27 @@ testSkipIfWindows("local-agent - read logs with filters", async ({ po }) => {
// - Client logs from last minute // - Client logs from last minute
await po.sendPrompt("tc=local-agent/read-logs"); await po.sendPrompt("tc=local-agent/read-logs");
await po.snapshotMessages(); await expect(
po.page.getByText(
"Let me check the recent console logs to see what's happening in the application.",
),
).toBeVisible();
await expect(
po.page.getByRole("button", { name: /LOGS Reading \d+ logs$/ }),
).toBeVisible();
await expect(
po.page.getByRole("button", {
name: /LOGS Reading \d+ logs \(level: error\)/,
}),
).toBeVisible();
await expect(
po.page.getByRole("button", {
name: /LOGS Reading \d+ logs \(type: client\)/,
}),
).toBeVisible();
await expect(
po.page.getByText(
"I've reviewed the console logs. The application appears to be running normally with no critical errors detected.",
),
).toBeVisible();
}); });
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
"type": "function", "type": "function",
"function": { "function": {
"name": "list_files", "name": "list_files",
"description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. If you are not sure, list all files by omitting the directory parameter.", "description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. Use include_ignored=true to include git-ignored and hidden paths; recursive ignored listings require directory to be set. Results are capped at 1000 paths.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -99,8 +99,8 @@ ...@@ -99,8 +99,8 @@
"description": "Whether to list files recursively (default: false)", "description": "Whether to list files recursively (default: false)",
"type": "boolean" "type": "boolean"
}, },
"include_hidden": { "include_ignored": {
"description": "Whether to include .dyad files which are git-ignored (default: false)", "description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false).",
"type": "boolean" "type": "boolean"
} }
}, },
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
"type": "function", "type": "function",
"function": { "function": {
"name": "grep", "name": "grep",
"description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.", "description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Use include_ignored=true to search git-ignored and hidden files/directories such as node_modules. Pair it with include_pattern to keep searches scoped.\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -129,6 +129,10 @@ ...@@ -129,6 +129,10 @@
"description": "Glob pattern for files to exclude", "description": "Glob pattern for files to exclude",
"type": "string" "type": "string"
}, },
"include_ignored": {
"description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false). Use include_pattern to keep this scoped.",
"type": "boolean"
},
"case_sensitive": { "case_sensitive": {
"description": "Whether the search should be case sensitive (default: false)", "description": "Whether the search should be case sensitive (default: false)",
"type": "boolean" "type": "boolean"
......
...@@ -252,7 +252,7 @@ ...@@ -252,7 +252,7 @@
{ {
"type": "function", "type": "function",
"name": "list_files", "name": "list_files",
"description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. If you are not sure, list all files by omitting the directory parameter.", "description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. Use include_ignored=true to include git-ignored and hidden paths; recursive ignored listings require directory to be set. Results are capped at 1000 paths.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -265,8 +265,8 @@ ...@@ -265,8 +265,8 @@
"description": "Whether to list files recursively (default: false)", "description": "Whether to list files recursively (default: false)",
"type": "boolean" "type": "boolean"
}, },
"include_hidden": { "include_ignored": {
"description": "Whether to include .dyad files which are git-ignored (default: false)", "description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false).",
"type": "boolean" "type": "boolean"
} }
}, },
...@@ -276,7 +276,7 @@ ...@@ -276,7 +276,7 @@
{ {
"type": "function", "type": "function",
"name": "grep", "name": "grep",
"description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.", "description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Use include_ignored=true to search git-ignored and hidden files/directories such as node_modules. Pair it with include_pattern to keep searches scoped.\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -293,6 +293,10 @@ ...@@ -293,6 +293,10 @@
"description": "Glob pattern for files to exclude", "description": "Glob pattern for files to exclude",
"type": "string" "type": "string"
}, },
"include_ignored": {
"description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false). Use include_pattern to keep this scoped.",
"type": "boolean"
},
"case_sensitive": { "case_sensitive": {
"description": "Whether the search should be case sensitive (default: false)", "description": "Whether the search should be case sensitive (default: false)",
"type": "boolean" "type": "boolean"
......
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
"type": "function", "type": "function",
"function": { "function": {
"name": "list_files", "name": "list_files",
"description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. If you are not sure, list all files by omitting the directory parameter.", "description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. Use include_ignored=true to include git-ignored and hidden paths; recursive ignored listings require directory to be set. Results are capped at 1000 paths.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -264,8 +264,8 @@ ...@@ -264,8 +264,8 @@
"description": "Whether to list files recursively (default: false)", "description": "Whether to list files recursively (default: false)",
"type": "boolean" "type": "boolean"
}, },
"include_hidden": { "include_ignored": {
"description": "Whether to include .dyad files which are git-ignored (default: false)", "description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false).",
"type": "boolean" "type": "boolean"
} }
}, },
...@@ -277,7 +277,7 @@ ...@@ -277,7 +277,7 @@
"type": "function", "type": "function",
"function": { "function": {
"name": "grep", "name": "grep",
"description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.", "description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')\n- Use include_ignored=true to search git-ignored and hidden files/directories such as node_modules. Pair it with include_pattern to keep searches scoped.\n- Results are limited to 100 matches by default (max 250). If results are truncated, narrow your search with include_pattern or a more specific query.",
"parameters": { "parameters": {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"type": "object", "type": "object",
...@@ -294,6 +294,10 @@ ...@@ -294,6 +294,10 @@
"description": "Glob pattern for files to exclude", "description": "Glob pattern for files to exclude",
"type": "string" "type": "string"
}, },
"include_ignored": {
"description": "Whether to include git-ignored and hidden files/directories such as node_modules (default: false). Use include_pattern to keep this scoped.",
"type": "boolean"
},
"case_sensitive": { "case_sensitive": {
"description": "Whether the search should be case sensitive (default: false)", "description": "Whether the search should be case sensitive (default: false)",
"type": "boolean" "type": "boolean"
......
- button "src - src/App.tsx - src/main.tsx - src/vite-env.d.ts (3 files total)" [expanded]: - button "src - src/App.tsx - src/main.tsx - src/vite-env.d.ts (3 paths total)" [expanded]:
- img - img
- text: "" - text: ""
- img - img
......
- button "src recursive - src/App.tsx - src/main.tsx - src/vite-env.d.ts (3 files total)" [expanded]: - button "src recursive - src/App.tsx - src/main.tsx - src/vite-env.d.ts (3 paths total)" [expanded]:
- img - img
- text: "" - text: ""
- img - img
......
- button ".dyad recursive include ignored - .dyad/ - .dyad/plans/ - .dyad/plans/test-plan.md (3 paths total)" [expanded]:
- img
- text: ""
- img
- text: ""
\ No newline at end of file
- 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\./
- button "file1.txt file1.txt Edit":
- img
- text: ""
- button "Edit":
- img
- text: ""
- img
- paragraph: More EOM
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- paragraph: tc=local-agent/read-logs
- paragraph: Let me check the recent console logs to see what's happening in the application.
- button /LOGS Reading \d+ logs/:
- img
- text: ""
- img
- paragraph: Now let me filter for only error logs to identify any issues.
- 'button "LOGS Reading 0 logs (level: error)"':
- img
- text: ""
- img
- paragraph: Let me also check client-side logs specifically.
- 'button "LOGS Reading 0 logs (type: client)"':
- img
- text: ""
- img
- paragraph: I've reviewed the console logs. The application appears to be running normally with no critical errors detected.
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- button "Undo":
- img
- text: ""
- button "Retry":
- img
- text: ""
\ No newline at end of file
...@@ -15,7 +15,7 @@ interface DyadListFilesProps { ...@@ -15,7 +15,7 @@ interface DyadListFilesProps {
properties: { properties: {
directory?: string; directory?: string;
recursive?: string; recursive?: string;
include_hidden?: string; include_ignored?: string;
state?: CustomTagState; state?: CustomTagState;
}; };
}; };
...@@ -23,10 +23,10 @@ interface DyadListFilesProps { ...@@ -23,10 +23,10 @@ interface DyadListFilesProps {
} }
export function DyadListFiles({ node, children }: DyadListFilesProps) { export function DyadListFiles({ node, children }: DyadListFilesProps) {
const { directory, recursive, include_hidden, state } = node.properties; const { directory, recursive, include_ignored, state } = node.properties;
const isLoading = state === "pending"; const isLoading = state === "pending";
const isRecursive = recursive === "true"; const isRecursive = recursive === "true";
const isIncludeHidden = include_hidden === "true"; const isIncludeIgnored = include_ignored === "true";
const content = typeof children === "string" ? children : ""; const content = typeof children === "string" ? children : "";
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
...@@ -45,7 +45,9 @@ export function DyadListFiles({ node, children }: DyadListFilesProps) { ...@@ -45,7 +45,9 @@ export function DyadListFiles({ node, children }: DyadListFilesProps) {
{title} {title}
</span> </span>
{isRecursive && <DyadBadge color="slate">recursive</DyadBadge>} {isRecursive && <DyadBadge color="slate">recursive</DyadBadge>}
{isIncludeHidden && <DyadBadge color="slate">include hidden</DyadBadge>} {isIncludeIgnored && (
<DyadBadge color="slate">include ignored</DyadBadge>
)}
{isLoading && ( {isLoading && (
<DyadStateIndicator state="pending" pendingLabel="Listing..." /> <DyadStateIndicator state="pending" pendingLabel="Listing..." />
)} )}
......
...@@ -710,7 +710,8 @@ function renderCustomTag( ...@@ -710,7 +710,8 @@ function renderCustomTag(
properties: { properties: {
directory: attributes.directory || "", directory: attributes.directory || "",
recursive: attributes.recursive || "", recursive: attributes.recursive || "",
include_hidden: attributes.include_hidden || "", include_ignored:
attributes.include_ignored || attributes.include_hidden || "",
state: getState({ isStreaming, inProgress }), state: getState({ isStreaming, inProgress }),
}, },
}} }}
......
...@@ -289,6 +289,63 @@ function deepHello() { ...@@ -289,6 +289,63 @@ function deepHello() {
expect(result).toContain("test1.ts"); expect(result).toContain("test1.ts");
expect(result).not.toContain("node_modules"); expect(result).not.toContain("node_modules");
}); });
it("searches node_modules when include_ignored is true", async () => {
const nodeModulesDir = path.join(testDir, "node_modules", "some-pkg");
await fs.promises.mkdir(nodeModulesDir, { recursive: true });
await fs.promises.writeFile(
path.join(nodeModulesDir, "index.js"),
`function dependencyHello() { return "hello from node_modules"; }`,
);
const result = await grepTool.execute(
{
query: "dependencyHello",
include_ignored: true,
include_pattern: "node_modules/some-pkg/**",
},
mockContext,
);
expect(result).toContain("node_modules/some-pkg/index.js");
expect(result).toContain("dependencyHello");
});
it("searches hidden ignored files when include_ignored is true", async () => {
const dyadDir = path.join(testDir, ".dyad");
await fs.promises.mkdir(dyadDir, { recursive: true });
await fs.promises.writeFile(
path.join(dyadDir, "backup.txt"),
"hiddenIgnoredNeedle",
);
const result = await grepTool.execute(
{
query: "hiddenIgnoredNeedle",
include_ignored: true,
include_pattern: ".dyad/**",
},
mockContext,
);
expect(result).toContain(".dyad/backup.txt");
});
it("keeps .git excluded when include_ignored is true", async () => {
const gitDir = path.join(testDir, ".git");
await fs.promises.mkdir(gitDir, { recursive: true });
await fs.promises.writeFile(
path.join(gitDir, "config"),
"gitIgnoredNeedle",
);
const result = await grepTool.execute(
{ query: "gitIgnoredNeedle", include_ignored: true },
mockContext,
);
expect(result).toBe("No matches found.");
});
}); });
describe("execute - regex patterns", () => { describe("execute - regex patterns", () => {
...@@ -357,6 +414,35 @@ function deepHello() { ...@@ -357,6 +414,35 @@ function deepHello() {
expect.stringContaining('total="'), expect.stringContaining('total="'),
); );
}); });
it("stops ignored searches after collecting enough matches", async () => {
const nodeModulesDir = path.join(testDir, "node_modules", "many-pkg");
await fs.promises.mkdir(nodeModulesDir, { recursive: true });
await Promise.all(
Array.from({ length: 20 }, (_, index) =>
fs.promises.writeFile(
path.join(nodeModulesDir, `file-${index}.js`),
"ignoredSearchNeedle\n",
),
),
);
const result = await grepTool.execute(
{
query: "ignoredSearchNeedle",
include_ignored: true,
include_pattern: "node_modules/many-pkg/**",
limit: 3,
},
mockContext,
);
const matchLines = result
.split("\n")
.filter((line) => line.match(/:\d+:/));
expect(matchLines).toHaveLength(3);
expect(result).toContain("[TRUNCATED: Showing 3 of at least 4 matches.");
});
}); });
describe("execute - result sorting", () => { describe("execute - result sorting", () => {
...@@ -460,6 +546,14 @@ function deepHello() { ...@@ -460,6 +546,14 @@ function deepHello() {
expect(result).toContain('exclude="*.md"'); expect(result).toContain('exclude="*.md"');
}); });
it("includes include_ignored in attributes", () => {
const result = grepTool.buildXml?.(
{ query: "test", include_ignored: true },
false,
);
expect(result).toContain('include_ignored="true"');
});
it("includes case-sensitive in attributes when true", () => { it("includes case-sensitive in attributes when true", () => {
const result = grepTool.buildXml?.( const result = grepTool.buildXml?.(
{ query: "test", case_sensitive: true }, { query: "test", case_sensitive: true },
...@@ -482,5 +576,13 @@ function deepHello() { ...@@ -482,5 +576,13 @@ function deepHello() {
}); });
expect(preview).toBe('Search for "hello" in *.ts'); expect(preview).toBe('Search for "hello" in *.ts');
}); });
it("includes include_ignored in preview", () => {
const preview = grepTool.getConsentPreview?.({
query: "hello",
include_ignored: true,
});
expect(preview).toBe('Search for "hello" including ignored files');
});
}); });
}); });
...@@ -31,6 +31,12 @@ const grepSchema = z.object({ ...@@ -31,6 +31,12 @@ const grepSchema = z.object({
.string() .string()
.optional() .optional()
.describe("Glob pattern for files to exclude"), .describe("Glob pattern for files to exclude"),
include_ignored: z
.boolean()
.optional()
.describe(
"Whether to include git-ignored and hidden files/directories such as node_modules (default: false). Use include_pattern to keep this scoped.",
),
case_sensitive: z case_sensitive: z
.boolean() .boolean()
.optional() .optional()
...@@ -66,6 +72,9 @@ function buildGrepAttributes( ...@@ -66,6 +72,9 @@ function buildGrepAttributes(
if (args.exclude_pattern) { if (args.exclude_pattern) {
attrs.push(`exclude="${escapeXmlAttr(args.exclude_pattern)}"`); attrs.push(`exclude="${escapeXmlAttr(args.exclude_pattern)}"`);
} }
if (args.include_ignored) {
attrs.push(`include_ignored="true"`);
}
if (args.case_sensitive) { if (args.case_sensitive) {
attrs.push(`case-sensitive="true"`); attrs.push(`case-sensitive="true"`);
} }
...@@ -91,16 +100,21 @@ async function runRipgrep({ ...@@ -91,16 +100,21 @@ async function runRipgrep({
query, query,
includePat, includePat,
excludePat, excludePat,
includeIgnored,
caseSensitive, caseSensitive,
maxMatches,
}: { }: {
appPath: string; appPath: string;
query: string; query: string;
includePat?: string; includePat?: string;
excludePat?: string; excludePat?: string;
includeIgnored?: boolean;
caseSensitive?: boolean; caseSensitive?: boolean;
}): Promise<RipgrepMatch[]> { maxMatches?: number;
}): Promise<{ matches: RipgrepMatch[]; stoppedEarly: boolean }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results: RipgrepMatch[] = []; const results: RipgrepMatch[] = [];
let stoppedEarly = false;
const args: string[] = [ const args: string[] = [
"--json", "--json",
"--no-config", "--no-config",
...@@ -108,6 +122,10 @@ async function runRipgrep({ ...@@ -108,6 +122,10 @@ async function runRipgrep({
`${MAX_FILE_SEARCH_SIZE}`, `${MAX_FILE_SEARCH_SIZE}`,
]; ];
if (includeIgnored) {
args.push("--no-ignore", "--hidden");
}
// Case sensitivity: default is case-insensitive // Case sensitivity: default is case-insensitive
if (!caseSensitive) { if (!caseSensitive) {
args.push("--ignore-case"); args.push("--ignore-case");
...@@ -126,7 +144,10 @@ async function runRipgrep({ ...@@ -126,7 +144,10 @@ async function runRipgrep({
// Exclusion globs come LAST so they always take precedence over any // Exclusion globs come LAST so they always take precedence over any
// include pattern (later --glob flags override earlier ones in ripgrep) // include pattern (later --glob flags override earlier ones in ripgrep)
args.push(...RIPGREP_EXCLUDED_GLOBS.flatMap((glob) => ["--glob", glob])); const exclusionGlobs = includeIgnored
? RIPGREP_EXCLUDED_GLOBS.filter((glob) => glob === "!.git/**")
: RIPGREP_EXCLUDED_GLOBS;
args.push(...exclusionGlobs.flatMap((glob) => ["--glob", glob]));
args.push("--", query, "."); args.push("--", query, ".");
...@@ -134,6 +155,10 @@ async function runRipgrep({ ...@@ -134,6 +155,10 @@ async function runRipgrep({
let buffer = ""; let buffer = "";
rg.stdout.on("data", (data) => { rg.stdout.on("data", (data) => {
if (stoppedEarly) {
return;
}
buffer += data.toString(); buffer += data.toString();
const lines = buffer.split("\n"); const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; buffer = lines.pop() ?? "";
...@@ -159,11 +184,23 @@ async function runRipgrep({ ...@@ -159,11 +184,23 @@ async function runRipgrep({
// Normalize path (remove leading ./) // Normalize path (remove leading ./)
const normalizedPath = matchPath.replace(/^\.\//, ""); const normalizedPath = matchPath.replace(/^\.\//, "");
if (maxMatches !== undefined && results.length >= maxMatches) {
stoppedEarly = true;
rg.kill();
break;
}
results.push({ results.push({
path: normalizedPath, path: normalizedPath,
lineNumber, lineNumber,
lineText: lineText.replace(/\r?\n$/, ""), lineText: lineText.replace(/\r?\n$/, ""),
}); });
if (maxMatches !== undefined && results.length >= maxMatches) {
stoppedEarly = true;
rg.kill();
break;
}
} catch { } catch {
// Skip malformed JSON lines // Skip malformed JSON lines
} }
...@@ -175,12 +212,17 @@ async function runRipgrep({ ...@@ -175,12 +212,17 @@ async function runRipgrep({
}); });
rg.on("close", (code) => { rg.on("close", (code) => {
if (stoppedEarly) {
resolve({ matches: results, stoppedEarly });
return;
}
// rg exits with code 1 when no matches are found; treat as success // rg exits with code 1 when no matches are found; treat as success
if (code !== 0 && code !== 1) { if (code !== 0 && code !== 1) {
reject(new Error(`ripgrep exited with code ${code}`)); reject(new Error(`ripgrep exited with code ${code}`));
return; return;
} }
resolve(results); resolve({ matches: results, stoppedEarly });
}); });
rg.on("error", (error) => { rg.on("error", (error) => {
...@@ -197,6 +239,7 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = { ...@@ -197,6 +239,7 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = {
- By default, the search is case-insensitive - By default, the search is case-insensitive
- Use include_pattern to filter by file type (e.g. '*.tsx') - Use include_pattern to filter by file type (e.g. '*.tsx')
- Use exclude_pattern to skip certain files (e.g. '*.test.ts') - Use exclude_pattern to skip certain files (e.g. '*.test.ts')
- Use include_ignored=true to search git-ignored and hidden files/directories such as node_modules. Pair it with include_pattern to keep searches scoped.
- Results are limited to ${DEFAULT_LIMIT} matches by default (max ${MAX_LIMIT}). If results are truncated, narrow your search with include_pattern or a more specific query.`, - Results are limited to ${DEFAULT_LIMIT} matches by default (max ${MAX_LIMIT}). If results are truncated, narrow your search with include_pattern or a more specific query.`,
inputSchema: grepSchema, inputSchema: grepSchema,
defaultConsent: "always", defaultConsent: "always",
...@@ -206,6 +249,9 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = { ...@@ -206,6 +249,9 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = {
if (args.include_pattern) { if (args.include_pattern) {
preview += ` in ${args.include_pattern}`; preview += ` in ${args.include_pattern}`;
} }
if (args.include_ignored) {
preview += " including ignored files";
}
return preview; return preview;
}, },
...@@ -222,23 +268,25 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = { ...@@ -222,23 +268,25 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = {
execute: async (args, ctx: AgentContext) => { execute: async (args, ctx: AgentContext) => {
const includePatWasWildcard = args.include_pattern === "*"; const includePatWasWildcard = args.include_pattern === "*";
const limit = Math.min(args.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
const allMatches = await runRipgrep({ const { matches: allMatches, stoppedEarly } = await runRipgrep({
appPath: ctx.appPath, appPath: ctx.appPath,
query: args.query, query: args.query,
includePat: args.include_pattern, includePat: args.include_pattern,
excludePat: args.exclude_pattern, excludePat: args.exclude_pattern,
includeIgnored: args.include_ignored,
caseSensitive: args.case_sensitive, caseSensitive: args.case_sensitive,
maxMatches: args.include_ignored ? limit + 1 : undefined,
}); });
const totalCount = allMatches.length; const totalCount = allMatches.length;
const limit = Math.min(args.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
// Sort for deterministic output (ripgrep's parallel execution can produce varying order) // Sort for deterministic output (ripgrep's parallel execution can produce varying order)
const sortedMatches = [...allMatches].sort( const sortedMatches = [...allMatches].sort(
(a, b) => a.path.localeCompare(b.path) || a.lineNumber - b.lineNumber, (a, b) => a.path.localeCompare(b.path) || a.lineNumber - b.lineNumber,
); );
const matches = sortedMatches.slice(0, limit); const matches = sortedMatches.slice(0, limit);
const wasTruncated = totalCount > limit; const wasTruncated = stoppedEarly || totalCount > limit;
const attrs = buildGrepAttributes(args, matches.length, totalCount); const attrs = buildGrepAttributes(args, matches.length, totalCount);
...@@ -255,7 +303,8 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = { ...@@ -255,7 +303,8 @@ export const grepTool: ToolDefinition<z.infer<typeof grepSchema>> = {
// Add truncation notice for the AI // Add truncation notice for the AI
if (wasTruncated) { if (wasTruncated) {
resultText += `\n\n[TRUNCATED: Showing ${matches.length} of ${totalCount} matches. Use include_pattern to narrow your search (e.g., include_pattern="*.tsx") or use a more specific query.]`; const totalText = stoppedEarly ? `at least ${totalCount}` : totalCount;
resultText += `\n\n[TRUNCATED: Showing ${matches.length} of ${totalText} matches. Use include_pattern to narrow your search (e.g., include_pattern="*.tsx") or use a more specific query.]`;
} }
// Warn the LLM that "*" was ignored so it doesn't retry with the same pattern // Warn the LLM that "*" was ignored so it doesn't retry with the same pattern
......
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { listFilesTool } from "./list_files";
import type { AgentContext } from "./types";
import { DyadErrorKind } from "@/errors/dyad_error";
vi.mock("electron-log", () => ({
default: {
scope: () => ({
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
},
}));
describe("listFilesTool", () => {
let testDir: string;
let mockContext: AgentContext;
beforeEach(async () => {
testDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "list-files-test-"),
);
await fs.promises.writeFile(path.join(testDir, "src.ts"), "source");
await fs.promises.mkdir(path.join(testDir, "node_modules", "pkg"), {
recursive: true,
});
await fs.promises.writeFile(
path.join(testDir, "node_modules", "pkg", "index.js"),
"dependency",
);
await fs.promises.mkdir(path.join(testDir, ".dyad"), { recursive: true });
await fs.promises.writeFile(
path.join(testDir, ".dyad", "snapshot.json"),
"{}",
);
await fs.promises.mkdir(path.join(testDir, ".git"), { recursive: true });
await fs.promises.writeFile(
path.join(testDir, ".git", "config"),
"should stay hidden",
);
mockContext = {
event: {} as any,
appId: 1,
appPath: testDir,
chatId: 1,
supabaseProjectId: null,
supabaseOrganizationSlug: null,
neonProjectId: null,
neonActiveBranchId: null,
frameworkType: null,
messageId: 1,
isSharedModulesChanged: false,
isDyadPro: false,
todos: [],
dyadRequestId: "test-request",
fileEditTracker: {},
onXmlStream: vi.fn(),
onXmlComplete: vi.fn(),
requireConsent: vi.fn().mockResolvedValue(true),
appendUserMessage: vi.fn(),
onUpdateTodos: vi.fn(),
};
});
afterEach(async () => {
await fs.promises.rm(testDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it("accepts include_ignored in the schema", () => {
expect(() =>
listFilesTool.inputSchema.parse({ include_ignored: true }),
).not.toThrow();
});
it("includes ignored files when include_ignored is true", async () => {
const result = await listFilesTool.execute(
{ directory: "node_modules", recursive: true, include_ignored: true },
mockContext,
);
expect(result).toContain(" - node_modules/pkg/");
expect(result).toContain(" - node_modules/pkg/index.js");
expect(result).not.toContain(".git/config");
});
it("lists directories before files", async () => {
const result = await listFilesTool.execute(
{ directory: "node_modules", recursive: true, include_ignored: true },
mockContext,
);
const directoryIndex = result.indexOf(" - node_modules/pkg/");
const fileIndex = result.indexOf(" - node_modules/pkg/index.js");
expect(directoryIndex).toBeGreaterThanOrEqual(0);
expect(fileIndex).toBeGreaterThanOrEqual(0);
expect(directoryIndex).toBeLessThan(fileIndex);
});
it("includes include_ignored in XML", async () => {
await listFilesTool.execute(
{ directory: "node_modules", recursive: true, include_ignored: true },
mockContext,
);
expect(mockContext.onXmlComplete).toHaveBeenCalledWith(
expect.stringContaining('include_ignored="true"'),
);
});
it("rejects recursive ignored listings without a directory", async () => {
await expect(
listFilesTool.execute(
{ recursive: true, include_ignored: true },
mockContext,
),
).rejects.toMatchObject({
kind: DyadErrorKind.Validation,
message:
"include_ignored=true with recursive=true requires a non-root directory to avoid listing too many files.",
});
});
it("rejects recursive ignored listings for the app root", async () => {
await expect(
listFilesTool.execute(
{ directory: ".", recursive: true, include_ignored: true },
mockContext,
),
).rejects.toMatchObject({
kind: DyadErrorKind.Validation,
message:
"include_ignored=true with recursive=true requires a non-root directory to avoid listing too many files.",
});
});
it("caps returned paths at 1000", async () => {
const generatedDir = path.join(testDir, "generated");
await fs.promises.mkdir(generatedDir);
await Promise.all(
Array.from({ length: 1005 }, (_, index) =>
fs.promises.writeFile(
path.join(generatedDir, `file-${String(index).padStart(4, "0")}.txt`),
"generated",
),
),
);
const result = await listFilesTool.execute(
{ directory: "generated", recursive: true, include_ignored: true },
mockContext,
);
const listedPathCount = result
.split("\n")
.filter((line) => line.startsWith(" - ")).length;
expect(listedPathCount).toBe(1000);
expect(result).toContain("[TRUNCATED: Showing 1000 of ");
expect(mockContext.onXmlComplete).toHaveBeenCalledWith(
expect.stringContaining('truncated="true"'),
);
});
});
...@@ -9,6 +9,9 @@ import { ...@@ -9,6 +9,9 @@ import {
} from "./types"; } from "./types";
import { extractCodebase } from "../../../../../../utils/codebase"; import { extractCodebase } from "../../../../../../utils/codebase";
import { resolveDirectoryWithinAppPath } from "./path_safety"; import { resolveDirectoryWithinAppPath } from "./path_safety";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
const MAX_PATHS_TO_RETURN = 1_000;
const listFilesSchema = z.object({ const listFilesSchema = z.object({
directory: z.string().optional().describe("Optional subdirectory to list"), directory: z.string().optional().describe("Optional subdirectory to list"),
...@@ -16,42 +19,64 @@ const listFilesSchema = z.object({ ...@@ -16,42 +19,64 @@ const listFilesSchema = z.object({
.boolean() .boolean()
.optional() .optional()
.describe("Whether to list files recursively (default: false)"), .describe("Whether to list files recursively (default: false)"),
include_hidden: z include_ignored: z
.boolean() .boolean()
.optional() .optional()
.describe( .describe(
"Whether to include .dyad files which are git-ignored (default: false)", "Whether to include git-ignored and hidden files/directories such as node_modules (default: false).",
), ),
}); });
type ListFilesArgs = z.infer<typeof listFilesSchema>; type ListFilesArgs = z.infer<typeof listFilesSchema>;
function getXmlAttributes(args: ListFilesArgs) { interface ListedPath {
path: string;
isDirectory: boolean;
}
function getDisplayPath(entry: ListedPath): string {
return entry.isDirectory ? `${entry.path}/` : entry.path;
}
function sortListedPaths(entries: ListedPath[]): ListedPath[] {
return [...entries].sort((a, b) => {
if (a.isDirectory !== b.isDirectory) {
return a.isDirectory ? -1 : 1;
}
return a.path.localeCompare(b.path);
});
}
function getXmlAttributes(args: ListFilesArgs, count?: number, total?: number) {
const dirAttr = args.directory const dirAttr = args.directory
? ` directory="${escapeXmlAttr(args.directory)}"` ? ` directory="${escapeXmlAttr(args.directory)}"`
: ""; : "";
const recursiveAttr = const recursiveAttr =
args.recursive !== undefined ? ` recursive="${args.recursive}"` : ""; args.recursive !== undefined ? ` recursive="${args.recursive}"` : "";
const includeHiddenAttr = const includeIgnoredAttr =
args.include_hidden !== undefined args.include_ignored !== undefined
? ` include_hidden="${args.include_hidden}"` ? ` include_ignored="${args.include_ignored}"`
: ""; : "";
return `${dirAttr}${recursiveAttr}${includeHiddenAttr}`; const countAttr = count !== undefined ? ` count="${count}"` : "";
const totalAttr =
total !== undefined && total > (count ?? 0) ? ` total="${total}"` : "";
const truncatedAttr = totalAttr ? ` truncated="true"` : "";
return `${dirAttr}${recursiveAttr}${includeIgnoredAttr}${countAttr}${totalAttr}${truncatedAttr}`;
} }
export const listFilesTool: ToolDefinition<ListFilesArgs> = { export const listFilesTool: ToolDefinition<ListFilesArgs> = {
name: "list_files", name: "list_files",
description: description:
"List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. If you are not sure, list all files by omitting the directory parameter.", "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. Use include_ignored=true to include git-ignored and hidden paths; recursive ignored listings require directory to be set. Results are capped at 1000 paths.",
inputSchema: listFilesSchema, inputSchema: listFilesSchema,
defaultConsent: "always", defaultConsent: "always",
getConsentPreview: (args) => { getConsentPreview: (args) => {
const recursiveText = args.recursive ? " (recursive)" : ""; const recursiveText = args.recursive ? " (recursive)" : "";
const hiddenText = args.include_hidden ? " (include hidden)" : ""; const ignoredText = args.include_ignored ? " (include ignored)" : "";
return args.directory return args.directory
? `List ${args.directory}${recursiveText}${hiddenText}` ? `List ${args.directory}${recursiveText}${ignoredText}`
: `List all files${recursiveText}${hiddenText}`; : `List all files${recursiveText}${ignoredText}`;
}, },
buildXml: (args, isComplete) => { buildXml: (args, isComplete) => {
...@@ -80,76 +105,87 @@ export const listFilesTool: ToolDefinition<ListFilesArgs> = { ...@@ -80,76 +105,87 @@ export const listFilesTool: ToolDefinition<ListFilesArgs> = {
sanitizedDirectory = normalizedRelativePath || undefined; sanitizedDirectory = normalizedRelativePath || undefined;
} }
if (args.include_ignored && args.recursive && !sanitizedDirectory) {
throw new DyadError(
"include_ignored=true with recursive=true requires a non-root directory to avoid listing too many files.",
DyadErrorKind.Validation,
);
}
// Use "**" for recursive, "*" for non-recursive (immediate children only) // Use "**" for recursive, "*" for non-recursive (immediate children only)
const globSuffix = args.recursive ? "/**" : "/*"; const globSuffix = args.recursive ? "/**" : "/*";
const globPath = sanitizedDirectory const globPath = sanitizedDirectory
? sanitizedDirectory + globSuffix ? sanitizedDirectory + globSuffix
: globSuffix.slice(1); // Remove leading "/" for root directory : globSuffix.slice(1); // Remove leading "/" for root directory
const { files } = await extractCodebase({ let allPaths: ListedPath[];
appPath: ctx.appPath,
chatContext: {
contextPaths: [{ globPath }],
smartContextAutoIncludes: [],
excludePaths: [],
},
});
// Build the list of file paths
let allFilePaths = files.map((file) => file.path);
// If include_hidden is true, also include .dyad files if (args.include_ignored) {
if (args.include_hidden) {
const normalizedAppPath = ctx.appPath.replace(/\\/g, "/"); const normalizedAppPath = ctx.appPath.replace(/\\/g, "/");
// Always search .dyad at the app root, regardless of directory filter const globPattern = `${normalizedAppPath}/${globPath}`;
const dyadGlobPattern = `${normalizedAppPath}/.dyad${globSuffix}`; const ignoredPaths = await glob(globPattern, {
withFileTypes: true,
const dyadFiles = await glob(dyadGlobPattern, { dot: true,
nodir: true, ignore: ["**/.git", "**/.git/**"],
absolute: true,
ignore: [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/.venv/**",
"**/venv/**",
],
}); });
// Convert to relative paths and add to the list allPaths = sortListedPaths(
const dyadRelativePaths = dyadFiles.map((file) => ignoredPaths.map((entry) => ({
path.relative(ctx.appPath, file).split(path.sep).join("/"), path: path
.relative(ctx.appPath, entry.fullpath())
.split(path.sep)
.join("/"),
isDirectory: entry.isDirectory(),
})),
); );
} else {
const { files } = await extractCodebase({
appPath: ctx.appPath,
chatContext: {
contextPaths: [{ globPath }],
smartContextAutoIncludes: [],
excludePaths: [],
},
});
// Deduplicate and sort // Build the list of file paths
allFilePaths = [ allPaths = sortListedPaths(
...new Set([...allFilePaths, ...dyadRelativePaths]), files.map((file) => ({
].sort(); path: file.path,
isDirectory: false,
})),
);
} }
const totalCount = allPaths.length;
const cappedPaths = allPaths.slice(0, MAX_PATHS_TO_RETURN);
const wasTruncated = totalCount > cappedPaths.length;
// Build full file list for LLM // Build full file list for LLM
const allFilesList = const allFilesList =
allFilePaths.map((filePath) => " - " + filePath).join("\n") || ""; cappedPaths.map((entry) => " - " + getDisplayPath(entry)).join("\n") ||
"";
const resultText = wasTruncated
? `${allFilesList}\n\n[TRUNCATED: Showing ${cappedPaths.length} of ${totalCount} paths. Use directory to narrow the listing.]`
: allFilesList;
// Build abbreviated list for UI display // Build abbreviated list for UI display
const MAX_FILES_TO_SHOW = 20; const MAX_FILES_TO_SHOW = 20;
const totalCount = allFilePaths.length; const displayedFiles = cappedPaths.slice(0, MAX_FILES_TO_SHOW);
const displayedFiles = allFilePaths.slice(0, MAX_FILES_TO_SHOW);
const abbreviatedList = const abbreviatedList =
displayedFiles.map((filePath) => " - " + filePath).join("\n") || ""; displayedFiles.map((entry) => " - " + getDisplayPath(entry)).join("\n") ||
"";
const countInfo = const countInfo =
totalCount > MAX_FILES_TO_SHOW totalCount > MAX_FILES_TO_SHOW
? `\n... and ${totalCount - MAX_FILES_TO_SHOW} more files (${totalCount} total)` ? `\n... and ${totalCount - MAX_FILES_TO_SHOW} more paths (${totalCount} total)`
: `\n(${totalCount} files total)`; : `\n(${totalCount} paths total)`;
// Write abbreviated list to UI // Write abbreviated list to UI
ctx.onXmlComplete( ctx.onXmlComplete(
`<dyad-list-files${getXmlAttributes(args)}>${escapeXmlContent(abbreviatedList + countInfo)}</dyad-list-files>`, `<dyad-list-files${getXmlAttributes(args, cappedPaths.length, totalCount)}>${escapeXmlContent(abbreviatedList + countInfo)}</dyad-list-files>`,
); );
// Return full file list for LLM // Return full file list for LLM
return allFilesList; return resultText;
}, },
}; };
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论