import { z } from "zod" import * as fs from "fs" import * as path from "path" import { Tool } from "./tool" import { LSP } from "../lsp" import { FileTimes } from "./util/file-times" const MAX_READ_SIZE = 250 * 1024 const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data. WHEN TO USE THIS TOOL: - Use when you need to read the contents of a specific file - Helpful for examining source code, configuration files, or log files - Perfect for looking at text-based file formats HOW TO USE: - Provide the path to the file you want to view - Optionally specify an offset to start reading from a specific line - Optionally specify a limit to control how many lines are read FEATURES: - Displays file contents with line numbers for easy reference - Can read from any position in a file using the offset parameter - Handles large files by limiting the number of lines read - Automatically truncates very long lines for better display - Suggests similar file names when the requested file isn't found LIMITATIONS: - Maximum file size is 250KB - Default reading limit is 2000 lines - Lines longer than 2000 characters are truncated - Cannot display binary files or images - Images can be identified but not displayed TIPS: - Use with Glob tool to first find files you want to view - For code exploration, first use Grep to find relevant files, then View to examine them - When viewing large files, use the offset parameter to read specific sections` export const ViewTool = Tool.define({ id: "opencode.view", description: DESCRIPTION, parameters: z.object({ filePath: z.string().describe("The path to the file to read"), offset: z .number() .describe("The line number to start reading from (0-based)") .optional(), limit: z .number() .describe("The number of lines to read (defaults to 2000)") .optional(), }), async execute(params, ctx) { let filePath = params.filePath if (!path.isAbsolute(filePath)) { filePath = path.join(process.cwd(), filePath) } const file = Bun.file(filePath) if (!(await file.exists())) { const dir = path.dirname(filePath) const base = path.basename(filePath) const dirEntries = fs.readdirSync(dir) const suggestions = dirEntries .filter( (entry) => entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()), ) .map((entry) => path.join(dir, entry)) .slice(0, 3) if (suggestions.length > 0) { throw new Error( `File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`, ) } throw new Error(`File not found: ${filePath}`) } const stats = await file.stat() if (stats.size > MAX_READ_SIZE) throw new Error( `File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`, ) const limit = params.limit ?? DEFAULT_READ_LIMIT const offset = params.offset || 0 const isImage = isImageFile(filePath) if (isImage) throw new Error( `This is an image file of type: ${isImage}\nUse a different tool to process images`, ) const lines = await file.text().then((text) => text.split("\n")) const raw = lines.slice(offset, offset + limit).map((line) => { return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line }) const content = raw.map((line, index) => { return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` }) const preview = raw.slice(0, 20).join("\n") let output = "\n" output += content.join("\n") if (lines.length > offset + content.length) { output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${ offset + content.length })` } output += "\n" // just warms the lsp client LSP.file(filePath) FileTimes.read(ctx.sessionID, filePath) return { output, metadata: { preview, }, } }, }) function isImageFile(filePath: string): string | false { const ext = path.extname(filePath).toLowerCase() switch (ext) { case ".jpg": case ".jpeg": return "JPEG" case ".png": return "PNG" case ".gif": return "GIF" case ".bmp": return "BMP" case ".svg": return "SVG" case ".webp": return "WebP" default: return false } }