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

agent: type check tool (#2137)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Introduces a TypeScript type-check capability and a compact status UI in chat. > > - Adds `run_type_checks` tool (registered in `tool_definitions`) that runs workspace or path-scoped checks via existing `generateProblemReport`; formats results as count plus `file:line:col` lines > - Streams progress and completion using `dyad-status` XML, gated by `enableAutoFixProblems`; consent preview reflects targeted paths > - Implements `DyadStatus` component for pending/success/error display with expandable details and registers `dyad-status` in `DyadMarkdownParser` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8e308551339675803703f01d3ef2018972904b01. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add an agent tool to run TypeScript type checks and stream results to chat using a new dyad-status UI block. Lets the agent scope checks to specific files or folders and shows clear progress and outcomes. - New Features - Added run_type_checks tool: runs TypeScript checks; accepts optional paths to limit scope. - Streams progress and results via a dyad-status block in chat; shows pending/success/error states. - Formats output with a count and file:line:col messages, or “No type errors found.” - Enabled only when Auto Fix Problems is on; consent preview lists targeted paths. - Added DyadStatus component and registered the dyad-status custom tag in DyadMarkdownParser. <sup>Written for commit 8e308551339675803703f01d3ef2018972904b01. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatargreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
上级 23f9a95e
......@@ -31,6 +31,7 @@ import { DyadListFiles } from "./DyadListFiles";
import { DyadDatabaseSchema } from "./DyadDatabaseSchema";
import { DyadSupabaseTableSchema } from "./DyadSupabaseTableSchema";
import { DyadSupabaseProjectInfo } from "./DyadSupabaseProjectInfo";
import { DyadStatus } from "./DyadStatus";
import { mapActionToButton } from "./ChatInput";
import { SuggestedAction } from "@/lib/schemas";
import { FixAllErrorsButton } from "./FixAllErrorsButton";
......@@ -63,6 +64,7 @@ const DYAD_CUSTOM_TAGS = [
"dyad-database-schema",
"dyad-supabase-table-schema",
"dyad-supabase-project-info",
"dyad-status",
];
interface DyadMarkdownParserProps {
......@@ -665,6 +667,20 @@ function renderCustomTag(
</DyadSupabaseProjectInfo>
);
case "dyad-status":
return (
<DyadStatus
node={{
properties: {
title: attributes.title || "Processing...",
state: getState({ isStreaming, inProgress }),
},
}}
>
{content}
</DyadStatus>
);
default:
return null;
}
......
import React, { useState } from "react";
import { CustomTagState } from "./stateTypes";
import {
Loader2,
CheckCircle2,
XCircle,
ChevronsDownUp,
ChevronsUpDown,
} from "lucide-react";
interface DyadStatusProps {
node: {
properties: {
title?: string;
state?: CustomTagState;
};
};
children?: React.ReactNode;
}
export function DyadStatus({ node, children }: DyadStatusProps) {
const { title = "Processing...", state } = node.properties;
const isInProgress = state === "pending";
const isAborted = state === "aborted";
const content = typeof children === "string" ? children : "";
const [isContentVisible, setIsContentVisible] = useState(false);
return (
<div
className={`bg-(--background-lightest) hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
isInProgress
? "border-amber-500"
: isAborted
? "border-red-500"
: "border-border"
}`}
onClick={() => setIsContentVisible(!isContentVisible)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isInProgress ? (
<Loader2 className="size-4 animate-spin text-amber-600" />
) : isAborted ? (
<XCircle className="size-4 text-red-600" />
) : (
<CheckCircle2 className="size-4 text-green-600 dark:text-green-500" />
)}
<span
className={`font-medium text-sm ${
isInProgress
? "bg-gradient-to-r from-foreground via-muted-foreground to-foreground bg-[length:200%_100%] animate-[shimmer_2s_ease-in-out_infinite] bg-clip-text text-transparent"
: "text-gray-700 dark:text-gray-300"
}`}
>
{title}
</span>
</div>
<div className="flex items-center">
{isContentVisible ? (
<ChevronsDownUp
size={20}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
/>
) : (
<ChevronsUpDown
size={20}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
/>
)}
</div>
</div>
{isContentVisible && content && (
<div
className="mt-2 p-3 text-xs font-mono whitespace-pre-wrap max-h-60 overflow-y-auto bg-muted/20 rounded cursor-text"
onClick={(e) => e.stopPropagation()}
>
{content}
</div>
)}
</div>
);
}
......@@ -23,6 +23,7 @@ import { editFileTool } from "./tools/edit_file";
import { webSearchTool } from "./tools/web_search";
import { webCrawlTool } from "./tools/web_crawl";
import { updateTodosTool } from "./tools/update_todos";
import { runTypeChecksTool } from "./tools/run_type_checks";
import type { LanguageModelV3ToolResultOutput } from "@ai-sdk/provider";
import {
escapeXmlAttr,
......@@ -53,6 +54,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [
webSearchTool,
webCrawlTool,
updateTodosTool,
runTypeChecksTool,
];
// ============================================================================
// Agent Tool Name Type (derived from TOOL_DEFINITIONS)
......
import { z } from "zod";
import {
ToolDefinition,
AgentContext,
escapeXmlAttr,
escapeXmlContent,
} from "./types";
import { generateProblemReport } from "@/ipc/processors/tsc";
import type { Problem } from "@/ipc/ipc_types";
import { readSettings } from "@/main/settings";
import { normalizePath } from "../../../../../../../shared/normalizePath";
const runTypeChecksSchema = z.object({
paths: z
.array(z.string())
.optional()
.describe(
"Optional. An array of paths to files or directories to read type errors for. If provided, returns diagnostics for the specified files/directories only. If not provided, returns diagnostics for all files in the workspace.",
),
});
/**
* Check if a problem file matches any of the specified paths.
* Matches if the problem file equals the path (file match) or
* starts with the path followed by a separator (directory match).
*/
function matchesPaths(problemFile: string, paths: string[]): boolean {
// Normalize the problem file path (convert backslashes and remove leading ./)
const normalizedProblemFile = normalizePath(problemFile).replace(/^\.\//, "");
for (const targetPath of paths) {
// Normalize target path (convert backslashes, remove leading ./ and trailing /)
const normalizedTarget = normalizePath(targetPath)
.replace(/^\.\//, "")
.replace(/\/$/, "");
// Exact file match
if (normalizedProblemFile === normalizedTarget) {
return true;
}
// Directory prefix match (problem file is inside the target directory)
if (normalizedProblemFile.startsWith(normalizedTarget + "/")) {
return true;
}
}
return false;
}
/**
* Format problems into a readable text output for the agent.
*/
function formatProblems(problems: Problem[]): string {
if (problems.length === 0) {
return "No type errors found.";
}
const lines = problems.map(
(p) => `${p.file}:${p.line}:${p.column}: ${p.message}`,
);
return `Found ${problems.length} type error(s):\n\n${lines.join("\n")}`;
}
export const runTypeChecksTool: ToolDefinition<
z.infer<typeof runTypeChecksSchema>
> = {
name: "run_type_checks",
description: `Run TypeScript type checks on the current workspace. You can provide paths to specific files or directories, or omit the argument to get diagnostics for all files.
- If a file path is provided, returns diagnostics for that file only
- If a directory path is provided, returns diagnostics for all files within that directory
- If no path is provided, returns diagnostics for all files in the workspace
- This tool can return type errors that were already present before your edits, so avoid calling it with a very wide scope of files
- NEVER call this tool on a file unless you've edited it or are about to edit it`,
inputSchema: runTypeChecksSchema,
defaultConsent: "always",
isEnabled: () => !!readSettings().enableAutoFixProblems,
getConsentPreview: (args) =>
args.paths && args.paths.length > 0
? `Check types for: ${args.paths.join(", ")}`
: "Check types for all files",
execute: async (args, ctx: AgentContext) => {
// Stream initial XML with in-progress state
const title =
args.paths && args.paths.length > 0
? `Type checking: ${args.paths.join(", ")}`
: "Type checking all files";
ctx.onXmlStream(
`<dyad-status title="${escapeXmlAttr(title)}"></dyad-status>`,
);
// Run TypeScript type checking using existing infrastructure
const problemReport = await generateProblemReport({
fullResponse: "",
appPath: ctx.appPath,
});
let problems = problemReport.problems;
// Filter by paths if specified
if (args.paths && args.paths.length > 0) {
problems = problems.filter((p) => matchesPaths(p.file, args.paths!));
}
const result = formatProblems(problems);
// Complete XML with result
ctx.onXmlComplete(
`<dyad-status title="${escapeXmlAttr(title)}">\n${escapeXmlContent(result)}\n</dyad-status>`,
);
return result;
},
};
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论