| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- 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 = "<file>\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</file>"
- // 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
- }
- }
|