Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const toolNames = [
"apply_patch",
"search_files",
"list_files",
"read_lints",
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",
Expand Down
31 changes: 31 additions & 0 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ export class NativeToolCallParser {
return undefined
}

private static coerceStringArray(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === "string")
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value) as unknown
return Array.isArray(parsed)
? (parsed as unknown[]).filter((item): item is string => typeof item === "string")
: []
} catch {
return []
}
}
return []
}

/**
* Process a raw tool call chunk from the API stream.
* Handles tracking, buffering, and emits start/delta/end events.
Expand Down Expand Up @@ -627,6 +644,14 @@ export class NativeToolCallParser {
}
break

case "read_lints":
if (partialArgs.paths !== undefined) {
nativeArgs = {
paths: this.coerceStringArray(partialArgs.paths),
}
}
break

case "new_task":
if (partialArgs.mode !== undefined || partialArgs.message !== undefined) {
nativeArgs = {
Expand Down Expand Up @@ -976,6 +1001,12 @@ export class NativeToolCallParser {
}
break

case "read_lints":
nativeArgs = {
paths: this.coerceStringArray(args.paths),
} as NativeArgsFor<TName>
break

case "new_task":
if (args.mode !== undefined && args.message !== undefined) {
nativeArgs = {
Expand Down
10 changes: 10 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { generateImageTool } from "../tools/GenerateImageTool"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { readLintsTool } from "../tools/ReadLintsTool"

import { formatResponse } from "../prompts/responses"
import { sanitizeToolUseId } from "../../utils/tool-id"
Expand Down Expand Up @@ -355,6 +356,8 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name}]`
case "list_files":
return `[${block.name} for '${block.params.path}']`
case "read_lints":
return block.params.paths ? `[${block.name} for paths]` : `[${block.name}]`
case "use_mcp_tool":
return `[${block.name} for '${block.params.server_name}']`
case "access_mcp_resource":
Expand Down Expand Up @@ -747,6 +750,13 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "read_lints":
await readLintsTool.handle(cline, block as ToolUse<"read_lints">, {
askApproval,
handleError,
pushToolResult,
})
break
case "codebase_search":
await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
askApproval,
Expand Down
2 changes: 2 additions & 0 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import executeCommand from "./execute_command"
import generateImage from "./generate_image"
import listFiles from "./list_files"
import newTask from "./new_task"
import readLints from "./read_lints"
import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
Expand Down Expand Up @@ -57,6 +58,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
generateImage,
listFiles,
newTask,
readLints,
readCommandOutput,
createReadFileTool(readFileOptions),
runSlashCommand,
Expand Down
38 changes: 38 additions & 0 deletions src/core/prompts/tools/native-tools/read_lints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type OpenAI from "openai"

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).

Parameters:
- 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).

Example: Get lints for files edited in this task
{ }

Example: Get lints for a specific file
{ "paths": ["src/foo.ts"] }

Example: Get lints for a directory
{ "paths": ["src"] }`

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.`

export default {
type: "function",
function: {
name: "read_lints",
description: READ_LINTS_DESCRIPTION,
strict: true,
parameters: {
type: "object",
properties: {
paths: {
type: "array",
items: { type: "string" },
description: PATHS_PARAMETER_DESCRIPTION,
},
},
required: [],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
11 changes: 11 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
diffViewProvider: DiffViewProvider
diffStrategy?: DiffStrategy
didEditFile: boolean = false
/** Relative paths (POSIX) of files written in this task, for read_lints "edited only" scope. */
editedFilePaths: Set<string> = new Set()

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

/**
* Record that a file was edited in this task (for read_lints "edited only" scope).
* Call this after successfully saving a file via DiffViewProvider.
*/
recordEditedFile(relPath: string): void {
const normalized = path.relative(this.cwd, path.resolve(this.cwd, relPath)).toPosix()
this.editedFilePaths.add(normalized)
}

// Lifecycle
// Start / Resume / Abort / Dispose

Expand Down
126 changes: 126 additions & 0 deletions src/core/tools/ReadLintsTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as path from "path"
import * as vscode from "vscode"

import { Task } from "../task/Task"
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import type { ToolUse } from "../../shared/tools"

import { BaseTool, ToolCallbacks } from "./BaseTool"

const NO_EDITS_MESSAGE =
"No files have been edited in this task yet. Edit a file, then use read_lints to see errors and warnings."
const NO_PROBLEMS_MESSAGE = "No errors or warnings detected."

interface ReadLintsParams {
paths?: string[]
}

/**
* Normalize a path to POSIX relative form for comparison with editedFilePaths.
*/
function toRelativePosix(cwd: string, absolutePath: string): string {
return path.relative(cwd, absolutePath).toPosix()
}

/**
* Check if a file URI is under a directory (both relative to cwd).
*/
function isUnderDir(fileRelPosix: string, dirRelPosix: string): boolean {
if (dirRelPosix === "." || dirRelPosix === "") {
return true
}
const norm = dirRelPosix.endsWith("/") ? dirRelPosix : dirRelPosix + "/"
return fileRelPosix === dirRelPosix || fileRelPosix.startsWith(norm)
}

export class ReadLintsTool extends BaseTool<"read_lints"> {
readonly name = "read_lints" as const

async execute(params: ReadLintsParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { pushToolResult, handleError } = callbacks
const { paths } = params
const cwd = task.cwd

try {
const state = await task.providerRef.deref()?.getState()
const includeDiagnosticMessages = state?.includeDiagnosticMessages ?? true
const maxDiagnosticMessages = state?.maxDiagnosticMessages ?? 50

if (!includeDiagnosticMessages) {
pushToolResult(NO_PROBLEMS_MESSAGE)
return
}

let diagnosticsTuples: [vscode.Uri, vscode.Diagnostic[]][] = []

if (paths === undefined || paths.length === 0) {
// No paths: return diagnostics only for files edited in this task
if (task.editedFilePaths.size === 0) {
pushToolResult(NO_EDITS_MESSAGE)
return
}
const allDiagnostics = vscode.languages.getDiagnostics()
const editedSet = task.editedFilePaths
for (const [uri, diags] of allDiagnostics) {
const relPosix = toRelativePosix(cwd, uri.fsPath)
if (editedSet.has(relPosix)) {
diagnosticsTuples.push([uri, diags])
}
}
} else {
// Paths provided: return diagnostics for those files/directories
const allDiagnostics = vscode.languages.getDiagnostics()
const dirPaths: string[] = []
const fileUris: vscode.Uri[] = []

for (const relPath of paths) {
if (!relPath || typeof relPath !== "string") continue
const absolutePath = path.resolve(cwd, relPath)
const uri = vscode.Uri.file(absolutePath)
try {
const stat = await vscode.workspace.fs.stat(uri)
if (stat.type === vscode.FileType.Directory) {
dirPaths.push(toRelativePosix(cwd, absolutePath))
} else {
fileUris.push(uri)
}
} catch {
// Path may not exist; treat as file and try getDiagnostics(uri)
fileUris.push(uri)
}
}

const seenUri = new Set<string>()
for (const uri of fileUris) {
const diags = vscode.languages.getDiagnostics(uri)
if (diags.length > 0) {
diagnosticsTuples.push([uri, diags])
seenUri.add(uri.toString())
}
}
for (const [uri, diags] of allDiagnostics) {
if (diags.length === 0 || seenUri.has(uri.toString())) continue
const fileRelPosix = toRelativePosix(cwd, uri.fsPath)
const included = dirPaths.some((dirRelPosix) => isUnderDir(fileRelPosix, dirRelPosix))
if (included) {
diagnosticsTuples.push([uri, diags])
}
}
}

const result = await diagnosticsToProblemsString(
diagnosticsTuples,
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
cwd,
true,
maxDiagnosticMessages,
)

pushToolResult(result.trim() ? result.trim() : NO_PROBLEMS_MESSAGE)
} catch (error) {
await handleError("reading lints", error instanceof Error ? error : new Error(String(error)))
}
}
}

export const readLintsTool = new ReadLintsTool()
Loading
Loading