Skip to content

Commit 76ab891

Browse files
author
Ruslan Andreev
committed
feat: Add read_lints tool
Integrate the read_lints tool into the assistant message handling and native tool call parser. Update task management to track edited file paths for read_lints functionality. Enhance tool parameter definitions and ensure proper registration in the native tools index.
1 parent 7118a14 commit 76ab891

10 files changed

Lines changed: 442 additions & 1 deletion

File tree

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const toolNames = [
3434
"apply_patch",
3535
"search_files",
3636
"list_files",
37+
"read_lints",
3738
"use_mcp_tool",
3839
"access_mcp_resource",
3940
"ask_followup_question",

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ export class NativeToolCallParser {
8989
return undefined
9090
}
9191

92+
private static coerceStringArray(value: unknown): string[] {
93+
if (Array.isArray(value)) {
94+
return value.filter((item): item is string => typeof item === "string")
95+
}
96+
if (typeof value === "string") {
97+
try {
98+
const parsed = JSON.parse(value) as unknown
99+
return Array.isArray(parsed)
100+
? (parsed as unknown[]).filter((item): item is string => typeof item === "string")
101+
: []
102+
} catch {
103+
return []
104+
}
105+
}
106+
return []
107+
}
108+
92109
/**
93110
* Process a raw tool call chunk from the API stream.
94111
* Handles tracking, buffering, and emits start/delta/end events.
@@ -627,6 +644,14 @@ export class NativeToolCallParser {
627644
}
628645
break
629646

647+
case "read_lints":
648+
if (partialArgs.paths !== undefined) {
649+
nativeArgs = {
650+
paths: this.coerceStringArray(partialArgs.paths),
651+
}
652+
}
653+
break
654+
630655
case "new_task":
631656
if (partialArgs.mode !== undefined || partialArgs.message !== undefined) {
632657
nativeArgs = {
@@ -976,6 +1001,12 @@ export class NativeToolCallParser {
9761001
}
9771002
break
9781003

1004+
case "read_lints":
1005+
nativeArgs = {
1006+
paths: this.coerceStringArray(args.paths),
1007+
} as NativeArgsFor<TName>
1008+
break
1009+
9791010
case "new_task":
9801011
if (args.mode !== undefined && args.message !== undefined) {
9811012
nativeArgs = {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { generateImageTool } from "../tools/GenerateImageTool"
3737
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
3838
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
3939
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
40+
import { readLintsTool } from "../tools/ReadLintsTool"
4041

4142
import { formatResponse } from "../prompts/responses"
4243
import { sanitizeToolUseId } from "../../utils/tool-id"
@@ -355,6 +356,8 @@ export async function presentAssistantMessage(cline: Task) {
355356
return `[${block.name}]`
356357
case "list_files":
357358
return `[${block.name} for '${block.params.path}']`
359+
case "read_lints":
360+
return block.params.paths ? `[${block.name} for paths]` : `[${block.name}]`
358361
case "use_mcp_tool":
359362
return `[${block.name} for '${block.params.server_name}']`
360363
case "access_mcp_resource":
@@ -747,6 +750,13 @@ export async function presentAssistantMessage(cline: Task) {
747750
pushToolResult,
748751
})
749752
break
753+
case "read_lints":
754+
await readLintsTool.handle(cline, block as ToolUse<"read_lints">, {
755+
askApproval,
756+
handleError,
757+
pushToolResult,
758+
})
759+
break
750760
case "codebase_search":
751761
await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
752762
askApproval,

src/core/prompts/tools/native-tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import executeCommand from "./execute_command"
1010
import generateImage from "./generate_image"
1111
import listFiles from "./list_files"
1212
import newTask from "./new_task"
13+
import readLints from "./read_lints"
1314
import readCommandOutput from "./read_command_output"
1415
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
1516
import runSlashCommand from "./run_slash_command"
@@ -57,6 +58,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
5758
generateImage,
5859
listFiles,
5960
newTask,
61+
readLints,
6062
readCommandOutput,
6163
createReadFileTool(readFileOptions),
6264
runSlashCommand,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type OpenAI from "openai"
2+
3+
const READ_LINTS_DESCRIPTION = `Read linter errors and warnings from the workspace. Use this after editing files to check for problems. If no paths are provided, returns diagnostics only for files that have been edited in this task. If paths are provided, returns diagnostics for those files or directories (relative to the current workspace directory).
4+
5+
Parameters:
6+
- paths: (optional) Array of file or directory paths to get diagnostics for. If omitted, returns diagnostics for files edited in this task (or a message if none edited yet).
7+
8+
Example: Get lints for files edited in this task
9+
{ }
10+
11+
Example: Get lints for a specific file
12+
{ "paths": ["src/foo.ts"] }
13+
14+
Example: Get lints for a directory
15+
{ "paths": ["src"] }`
16+
17+
const PATHS_PARAMETER_DESCRIPTION = `Optional array of file or directory paths (relative to workspace) to get diagnostics for. If omitted, returns diagnostics only for files edited in this task.`
18+
19+
export default {
20+
type: "function",
21+
function: {
22+
name: "read_lints",
23+
description: READ_LINTS_DESCRIPTION,
24+
strict: true,
25+
parameters: {
26+
type: "object",
27+
properties: {
28+
paths: {
29+
type: "array",
30+
items: { type: "string" },
31+
description: PATHS_PARAMETER_DESCRIPTION,
32+
},
33+
},
34+
required: [],
35+
additionalProperties: false,
36+
},
37+
},
38+
} satisfies OpenAI.Chat.ChatCompletionTool

src/core/task/Task.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
304304
diffViewProvider: DiffViewProvider
305305
diffStrategy?: DiffStrategy
306306
didEditFile: boolean = false
307+
/** Relative paths (POSIX) of files written in this task, for read_lints "edited only" scope. */
308+
editedFilePaths: Set<string> = new Set()
307309

308310
// LLM Messages & Chat Messages
309311
apiConversationHistory: ApiMessage[] = []
@@ -1876,6 +1878,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
18761878
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
18771879
}
18781880

1881+
/**
1882+
* Record that a file was edited in this task (for read_lints "edited only" scope).
1883+
* Call this after successfully saving a file via DiffViewProvider.
1884+
*/
1885+
recordEditedFile(relPath: string): void {
1886+
const normalized = path.relative(this.cwd, path.resolve(this.cwd, relPath)).toPosix()
1887+
this.editedFilePaths.add(normalized)
1888+
}
1889+
18791890
// Lifecycle
18801891
// Start / Resume / Abort / Dispose
18811892

src/core/tools/ReadLintsTool.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as path from "path"
2+
import * as vscode from "vscode"
3+
4+
import { Task } from "../task/Task"
5+
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
6+
import type { ToolUse } from "../../shared/tools"
7+
8+
import { BaseTool, ToolCallbacks } from "./BaseTool"
9+
10+
const NO_EDITS_MESSAGE =
11+
"No files have been edited in this task yet. Edit a file, then use read_lints to see errors and warnings."
12+
const NO_PROBLEMS_MESSAGE = "No errors or warnings detected."
13+
14+
interface ReadLintsParams {
15+
paths?: string[]
16+
}
17+
18+
/**
19+
* Normalize a path to POSIX relative form for comparison with editedFilePaths.
20+
*/
21+
function toRelativePosix(cwd: string, absolutePath: string): string {
22+
return path.relative(cwd, absolutePath).toPosix()
23+
}
24+
25+
/**
26+
* Check if a file URI is under a directory (both relative to cwd).
27+
*/
28+
function isUnderDir(fileRelPosix: string, dirRelPosix: string): boolean {
29+
if (dirRelPosix === "." || dirRelPosix === "") {
30+
return true
31+
}
32+
const norm = dirRelPosix.endsWith("/") ? dirRelPosix : dirRelPosix + "/"
33+
return fileRelPosix === dirRelPosix || fileRelPosix.startsWith(norm)
34+
}
35+
36+
export class ReadLintsTool extends BaseTool<"read_lints"> {
37+
readonly name = "read_lints" as const
38+
39+
async execute(params: ReadLintsParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
40+
const { pushToolResult, handleError } = callbacks
41+
const { paths } = params
42+
const cwd = task.cwd
43+
44+
try {
45+
const state = await task.providerRef.deref()?.getState()
46+
const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true
47+
const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50
48+
49+
if (!includeDiagnosticMessages) {
50+
pushToolResult(NO_PROBLEMS_MESSAGE)
51+
return
52+
}
53+
54+
let diagnosticsTuples: [vscode.Uri, vscode.Diagnostic[]][] = []
55+
56+
if (paths === undefined || paths.length === 0) {
57+
// No paths: return diagnostics only for files edited in this task
58+
if (task.editedFilePaths.size === 0) {
59+
pushToolResult(NO_EDITS_MESSAGE)
60+
return
61+
}
62+
const allDiagnostics = vscode.languages.getDiagnostics()
63+
const editedSet = task.editedFilePaths
64+
for (const [uri, diags] of allDiagnostics) {
65+
const relPosix = toRelativePosix(cwd, uri.fsPath)
66+
if (editedSet.has(relPosix)) {
67+
diagnosticsTuples.push([uri, diags])
68+
}
69+
}
70+
} else {
71+
// Paths provided: return diagnostics for those files/directories
72+
const allDiagnostics = vscode.languages.getDiagnostics()
73+
const dirPaths: string[] = []
74+
const fileUris: vscode.Uri[] = []
75+
76+
for (const relPath of paths) {
77+
if (!relPath || typeof relPath !== "string") continue
78+
const absolutePath = path.resolve(cwd, relPath)
79+
const uri = vscode.Uri.file(absolutePath)
80+
try {
81+
const stat = await vscode.workspace.fs.stat(uri)
82+
if (stat.type === vscode.FileType.Directory) {
83+
dirPaths.push(toRelativePosix(cwd, absolutePath))
84+
} else {
85+
fileUris.push(uri)
86+
}
87+
} catch {
88+
// Path may not exist; treat as file and try getDiagnostics(uri)
89+
fileUris.push(uri)
90+
}
91+
}
92+
93+
const seenUri = new Set<string>()
94+
for (const uri of fileUris) {
95+
const diags = vscode.languages.getDiagnostics(uri)
96+
if (diags.length > 0) {
97+
diagnosticsTuples.push([uri, diags])
98+
seenUri.add(uri.toString())
99+
}
100+
}
101+
for (const [uri, diags] of allDiagnostics) {
102+
if (diags.length === 0 || seenUri.has(uri.toString())) continue
103+
const fileRelPosix = toRelativePosix(cwd, uri.fsPath)
104+
const included = dirPaths.some((dirRelPosix) => isUnderDir(fileRelPosix, dirRelPosix))
105+
if (included) {
106+
diagnosticsTuples.push([uri, diags])
107+
}
108+
}
109+
}
110+
111+
const result = await diagnosticsToProblemsString(
112+
diagnosticsTuples,
113+
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
114+
cwd,
115+
true,
116+
maxDiagnosticMessages,
117+
)
118+
119+
pushToolResult(result.trim() ? result.trim() : NO_PROBLEMS_MESSAGE)
120+
} catch (error) {
121+
await handleError("reading lints", error instanceof Error ? error : new Error(String(error)))
122+
}
123+
}
124+
}
125+
126+
export const readLintsTool = new ReadLintsTool()

0 commit comments

Comments
 (0)