feat(opencode): fff search tools (#27802)

Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
This commit is contained in:
Dmitriy Kovalenko
2026-06-06 06:42:31 -07:00
committed by GitHub
parent 4814ab3a3d
commit 7d3d80f840
28 changed files with 1256 additions and 138 deletions
+21 -33
View File
@@ -1,9 +1,8 @@
import path from "path"
import { Schema } from "effect"
import { Effect, Option } from "effect"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
@@ -25,7 +24,7 @@ export const GrepTool = Tool.define(
"grep",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const rg = yield* Ripgrep.Service
const searchSvc = yield* Search.Service
const reference = yield* Reference.Service
return {
@@ -69,7 +68,7 @@ export const GrepTool = Tool.define(
const cwd = info?.type === "Directory" ? search : path.dirname(search)
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
const result = yield* rg.search({
const result = yield* searchSvc.search({
cwd,
pattern: params.pattern,
glob: params.include ? [params.include] : undefined,
@@ -83,38 +82,15 @@ export const GrepTool = Tool.define(
line: item.line_number,
text: item.lines.text,
}))
const times = new Map(
(yield* Effect.forEach(
[...new Set(rows.map((row) => row.path))],
Effect.fnUntraced(function* (file) {
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info || info.type === "Directory") return undefined
return [
file,
info.mtime.pipe(
Option.map((time) => time.getTime()),
Option.getOrElse(() => 0),
) ?? 0,
] as const
}),
{ concurrency: 16 },
)).filter((entry): entry is readonly [string, number] => Boolean(entry)),
)
const matches = rows.flatMap((row) => {
const mtime = times.get(row.path)
if (mtime === undefined) return []
return [{ ...row, mtime }]
})
matches.sort((a, b) => b.mtime - a.mtime)
const limit = 100
const truncated = matches.length > limit
const final = truncated ? matches.slice(0, limit) : matches
const truncated = rows.length > limit
const final = truncated ? rows.slice(0, limit) : rows
if (final.length === 0) return empty
const total = matches.length
const output = [`Found ${total} matches${truncated ? ` (showing first ${limit})` : ""}`]
const total = rows.length
const hasMore = truncated || result.hasNextPage
const output = [`Found ${total} matches${hasMore ? " (more matches available)" : ""}`]
let current = ""
for (const match of final) {
@@ -135,11 +111,23 @@ export const GrepTool = Tool.define(
)
}
if (result.hasNextPage) {
output.push("")
output.push(
`(Results truncated. Consider using a more specific path or pattern.)`,
)
}
if (result.partial) {
output.push("")
output.push("(Some paths were inaccessible and skipped)")
}
if (result.regexFallbackError) {
output.push("")
output.push(`(Regex fallback: ${result.regexFallbackError})`)
}
return {
title: params.pattern,
metadata: {