refactor(core): unify filesystem search service (#31566)

This commit is contained in:
Dax 2026-06-09 20:38:02 -04:00 committed by GitHub
commit a0409e64d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 962 additions and 2852 deletions

View file

@ -33,6 +33,7 @@ import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
const originalEnv = {
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
@ -54,6 +55,7 @@ const workspaceLayer = (experimentalWorkspaces: boolean) =>
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))),
)

View file

@ -4,6 +4,7 @@ import { FetchHttpClient } from "effect/unstable/http"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import path from "path"
import { pathToFileURL } from "url"
@ -55,7 +56,9 @@ const workspaceLayer = Workspace.layer.pipe(
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })),
)
const it = testEffect(Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(
Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
afterEach(async () => {
await disposeAllInstances()

View file

@ -8,6 +8,7 @@ import { mkdir } from "node:fs/promises"
import path from "node:path"
import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
@ -53,7 +54,7 @@ const it = testEffect(
InstanceLayer.layer,
Project.defaultLayer,
workspaceLayer,
),
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const instanceContextTestLayer = Layer.mergeAll(

View file

@ -16,6 +16,7 @@ import * as Socket from "effect/unstable/socket/Socket"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
import { mkdir } from "node:fs/promises"
import { registerAdapter } from "../../src/control-plane/adapters"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
@ -58,7 +59,7 @@ const it = testEffect(
InstanceLayer.layer,
Project.defaultLayer,
workspaceLayer,
),
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const instanceContextTestLayer = Layer.mergeAll(

View file

@ -9,6 +9,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServ
import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
@ -66,7 +67,7 @@ const it = testEffect(
workspaceLayer,
Database.defaultLayer,
httpApiLayer,
),
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
function pathFor(path: string, params: Record<string, string>) {

View file

@ -21,6 +21,7 @@ import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace"
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
import { Database } from "@opencode-ai/core/database/database"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Project } from "../../src/project/project"
import { Session } from "../../src/session/session"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
@ -58,7 +59,7 @@ const it = testEffect(
Project.defaultLayer,
workspaceLayer,
Socket.layerWebSocketConstructorGlobal,
),
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
type ProxiedRequest = {

View file

@ -11,6 +11,7 @@ import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/
import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
import { Session } from "@/session/session"
import { Database } from "@opencode-ai/core/database/database"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Server } from "../../src/server/server"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
@ -34,7 +35,7 @@ const it = testEffect(
InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)),
Database.defaultLayer,
httpApiLayer,
),
).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
function request(path: string, directory: string, init: RequestInit = {}) {

View file

@ -48,7 +48,7 @@ import { Snapshot } from "../../src/snapshot"
import { ToolRegistry } from "@/tool/registry"
import { Truncate } from "@/tool/truncate"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Format } from "../../src/format"
import { TestInstance } from "../fixture/fixture"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
@ -190,7 +190,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Search.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(todo),

View file

@ -57,7 +57,7 @@ import { ToolRegistry } from "@/tool/registry"
import { Truncate } from "@/tool/truncate"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Format } from "../../src/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
@ -135,7 +135,7 @@ function makeHttp() {
Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Search.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(todo),

View file

@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Effect, Layer } from "effect"
import { Session } from "@/session/session"
import { SessionPrompt } from "../../src/session/prompt"
@ -8,7 +9,9 @@ import { testEffect } from "../lib/effect"
// Skip tests if no API key is available
const hasApiKey = !!process.env.ANTHROPIC_API_KEY
const it = testEffect(Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer))
const it = testEffect(
Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer).pipe(Layer.provide(Ripgrep.defaultLayer)),
)
const live = hasApiKey ? it.instance : it.instance.skip
describe("StructuredOutput Integration", () => {

View file

@ -5,7 +5,7 @@ import { Cause, Effect, Exit, Layer } from "effect"
import { GlobTool } from "../../src/tool/glob"
import { SessionID, MessageID } from "../../src/session/schema"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Truncate } from "@/tool/truncate"
@ -23,7 +23,7 @@ const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
CrossSpawnSpawner.defaultLayer,
FSUtil.defaultLayer,
Search.defaultLayer,
Ripgrep.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
Git.defaultLayer,

View file

@ -11,7 +11,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { Truncate } from "@/tool/truncate"
import { Agent } from "../../src/agent/agent"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
import { Permission } from "../../src/permission"
@ -25,7 +25,7 @@ const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.mergeAll(
CrossSpawnSpawner.defaultLayer,
FSUtil.defaultLayer,
Search.defaultLayer,
Ripgrep.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
Git.defaultLayer,
@ -135,6 +135,25 @@ describe("tool.grep", () => {
}),
)
it.instance("does not report an unknown total when results are truncated", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* Effect.promise(() =>
Promise.all(
Array.from({ length: 101 }, (_, index) =>
Bun.write(path.join(test.directory, `match-${index}.txt`), "needle"),
),
),
)
const info = yield* GrepTool
const grep = yield* info.init()
const result = yield* grep.execute({ pattern: "needle", path: test.directory, include: "*.txt" }, ctx)
expect(result.output).toContain("(Results truncated. Consider using a more specific path or pattern.)")
expect(result.output).not.toMatch(/showing \d+ of \d+ matches/)
}),
)
it.instance("supports exact file paths", () =>
Effect.gen(function* () {
const test = yield* TestInstance

View file

@ -8,7 +8,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { LSP } from "@/lsp/lsp"
import { Permission } from "../../src/permission"
import { SessionID, MessageID } from "../../src/session/schema"
@ -50,7 +50,7 @@ const readLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
CrossSpawnSpawner.defaultLayer,
Instruction.defaultLayer,
LSP.defaultLayer,
Search.defaultLayer,
Ripgrep.defaultLayer,
Truncate.defaultLayer,
)

View file

@ -26,7 +26,7 @@ import { Instruction } from "@/session/instruction"
import { EventV2Bridge } from "@/event-v2-bridge"
import { FetchHttpClient } from "effect/unstable/http"
import { Format } from "@/format"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import * as Truncate from "@/tool/truncate"
import { InstanceState } from "@/effect/instance-state"
@ -66,7 +66,7 @@ const registryLayer = (opts: RegistryLayerOptions = {}) =>
Layer.provide(FetchHttpClient.layer),
Layer.provide(Format.defaultLayer),
Layer.provide(Layer.mergeAll(node, Database.defaultLayer)),
Layer.provide(Search.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Truncate.defaultLayer),
)
.pipe(Layer.provide(RuntimeFlags.layer(opts.flags ?? {})))

View file

@ -1,5 +1,6 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Cause, Effect, Exit, Layer } from "effect"
import { afterEach, describe, expect } from "bun:test"
import path from "path"
@ -28,7 +29,7 @@ afterEach(async () => {
const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node).pipe(Layer.provide(Ripgrep.defaultLayer)))
describe("tool.skill", () => {
it.instance("execute returns skill content block with files", () =>

View file

@ -7,6 +7,7 @@ import { BackgroundJob } from "@/background/job"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Config } from "@/config/config"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Session } from "@/session/session"
import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
@ -45,7 +46,7 @@ const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
ToolRegistry.defaultLayer,
Database.defaultLayer,
RuntimeFlags.layer(flags),
)
).pipe(Layer.provide(Ripgrep.defaultLayer))
const it = testEffect(layer())
const background = testEffect(layer({ experimentalBackgroundSubagents: true }))