2
0

view.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { z } from "zod"
  2. import * as fs from "fs"
  3. import * as path from "path"
  4. import { Tool } from "./tool"
  5. import { LSP } from "../lsp"
  6. import { FileTimes } from "./util/file-times"
  7. const MAX_READ_SIZE = 250 * 1024
  8. const DEFAULT_READ_LIMIT = 2000
  9. const MAX_LINE_LENGTH = 2000
  10. 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.
  11. WHEN TO USE THIS TOOL:
  12. - Use when you need to read the contents of a specific file
  13. - Helpful for examining source code, configuration files, or log files
  14. - Perfect for looking at text-based file formats
  15. HOW TO USE:
  16. - Provide the path to the file you want to view
  17. - Optionally specify an offset to start reading from a specific line
  18. - Optionally specify a limit to control how many lines are read
  19. FEATURES:
  20. - Displays file contents with line numbers for easy reference
  21. - Can read from any position in a file using the offset parameter
  22. - Handles large files by limiting the number of lines read
  23. - Automatically truncates very long lines for better display
  24. - Suggests similar file names when the requested file isn't found
  25. LIMITATIONS:
  26. - Maximum file size is 250KB
  27. - Default reading limit is 2000 lines
  28. - Lines longer than 2000 characters are truncated
  29. - Cannot display binary files or images
  30. - Images can be identified but not displayed
  31. TIPS:
  32. - Use with Glob tool to first find files you want to view
  33. - For code exploration, first use Grep to find relevant files, then View to examine them
  34. - When viewing large files, use the offset parameter to read specific sections`
  35. export const ViewTool = Tool.define({
  36. id: "opencode.view",
  37. description: DESCRIPTION,
  38. parameters: z.object({
  39. filePath: z.string().describe("The path to the file to read"),
  40. offset: z
  41. .number()
  42. .describe("The line number to start reading from (0-based)")
  43. .optional(),
  44. limit: z
  45. .number()
  46. .describe("The number of lines to read (defaults to 2000)")
  47. .optional(),
  48. }),
  49. async execute(params, ctx) {
  50. let filePath = params.filePath
  51. if (!path.isAbsolute(filePath)) {
  52. filePath = path.join(process.cwd(), filePath)
  53. }
  54. const file = Bun.file(filePath)
  55. if (!(await file.exists())) {
  56. const dir = path.dirname(filePath)
  57. const base = path.basename(filePath)
  58. const dirEntries = fs.readdirSync(dir)
  59. const suggestions = dirEntries
  60. .filter(
  61. (entry) =>
  62. entry.toLowerCase().includes(base.toLowerCase()) ||
  63. base.toLowerCase().includes(entry.toLowerCase()),
  64. )
  65. .map((entry) => path.join(dir, entry))
  66. .slice(0, 3)
  67. if (suggestions.length > 0) {
  68. throw new Error(
  69. `File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
  70. )
  71. }
  72. throw new Error(`File not found: ${filePath}`)
  73. }
  74. const stats = await file.stat()
  75. if (stats.size > MAX_READ_SIZE)
  76. throw new Error(
  77. `File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
  78. )
  79. const limit = params.limit ?? DEFAULT_READ_LIMIT
  80. const offset = params.offset || 0
  81. const isImage = isImageFile(filePath)
  82. if (isImage)
  83. throw new Error(
  84. `This is an image file of type: ${isImage}\nUse a different tool to process images`,
  85. )
  86. const lines = await file.text().then((text) => text.split("\n"))
  87. const raw = lines.slice(offset, offset + limit).map((line) => {
  88. return line.length > MAX_LINE_LENGTH
  89. ? line.substring(0, MAX_LINE_LENGTH) + "..."
  90. : line
  91. })
  92. const content = raw.map((line, index) => {
  93. return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
  94. })
  95. const preview = raw.slice(0, 20).join("\n")
  96. let output = "<file>\n"
  97. output += content.join("\n")
  98. if (lines.length > offset + content.length) {
  99. output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
  100. offset + content.length
  101. })`
  102. }
  103. output += "\n</file>"
  104. // just warms the lsp client
  105. LSP.file(filePath)
  106. FileTimes.read(ctx.sessionID, filePath)
  107. return {
  108. output,
  109. metadata: {
  110. preview,
  111. },
  112. }
  113. },
  114. })
  115. function isImageFile(filePath: string): string | false {
  116. const ext = path.extname(filePath).toLowerCase()
  117. switch (ext) {
  118. case ".jpg":
  119. case ".jpeg":
  120. return "JPEG"
  121. case ".png":
  122. return "PNG"
  123. case ".gif":
  124. return "GIF"
  125. case ".bmp":
  126. return "BMP"
  127. case ".svg":
  128. return "SVG"
  129. case ".webp":
  130. return "WebP"
  131. default:
  132. return false
  133. }
  134. }