refactor(core): simplify location filesystem (#31545)

This commit is contained in:
Dax 2026-06-09 14:28:45 -04:00 committed by GitHub
commit 132ef57272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1048 additions and 1416 deletions

View file

@ -121,6 +121,7 @@
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
"htmlparser2": "8.0.2",
"ignore": "7.0.5",
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"mime-types": "3.0.2",

View file

@ -92,7 +92,7 @@ for (const q of FILE_QUERIES) {
const t = performance.now()
const r = await run(Search.Service.use((svc) => svc.file({ cwd: dir, query: q, limit: FILE_LIMIT })))
console.log(
`[Search.file] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r?.length ?? "undefined (cache fallback)"} results)`,
`[Search.file] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} results)`,
)
}

View file

@ -38,7 +38,9 @@ const FileReadCommand = effectCmd({
description: "File path to read",
}),
handler: Effect.fn("Cli.debug.file.read")(function* (args) {
const content = yield* filesystem(FileSystem.Service.use((svc) => svc.read({ path: RelativePath.make(args.path) })))
const content = yield* filesystem(
FileSystem.Service.use((svc) => svc.read({ path: RelativePath.make(args.path) })),
)
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
}),
})
@ -53,7 +55,9 @@ const FileListCommand = effectCmd({
description: "File path to list",
}),
handler: Effect.fn("Cli.debug.file.list")(function* (args) {
const files = yield* filesystem(FileSystem.Service.use((svc) => svc.list({ path: RelativePath.make(args.path) })))
const files = yield* filesystem(
FileSystem.Service.use((svc) => svc.list({ path: RelativePath.make(args.path) })),
)
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
}),
})

View file

@ -4,8 +4,10 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import ignore from "ignore"
import path from "path"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
@ -35,40 +37,17 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const limit = ctx.query.limit ?? 10
const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
const started = performance.now()
// Prefer fff (frecency + fuzzy ranking) and trust its ordering. Fall back
// to the ripgrep-backed FileSystem.find when fff is unavailable.
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
if (fff !== undefined) {
yield* Effect.logInfo("find file", {
engine: "fff",
query: ctx.query.query,
kind,
directory,
limit,
results: fff.length,
duration: Math.round(performance.now() - started),
})
return fff
}
const fallback = (yield* filesystem(
FileSystem.Service.use((fs) =>
fs.find({
query: ctx.query.query,
limit,
type: ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : undefined),
}),
),
)).map((item) => item.path)
yield* Effect.logInfo("find file", {
engine: "ripgrep",
engine: "fff",
query: ctx.query.query,
kind,
directory,
limit,
results: fallback.length,
results: fff.length,
duration: Math.round(performance.now() - started),
})
return fallback
return fff.map((item) => item.path)
})
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
@ -78,19 +57,30 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
const directory = (yield* InstanceState.context).directory
return yield* filesystem(
FileSystem.Service.use((fs) =>
fs.list({ path: RelativePath.make(ctx.query.path) }).pipe(
Effect.map((items) =>
items.map((item) => ({
name: path.basename(item.path),
path: item.path,
absolute: path.join(directory, item.path),
type: item.type,
ignored: fs.isIgnored(item.path, item.type),
})),
Effect.gen(function* () {
const fs = yield* FileSystem.Service
const raw = yield* FSUtil.Service
const location = yield* Location.Service
const ignored = ignore()
const gitignore = yield* raw
.readFileString(path.join(location.project.directory, ".gitignore"))
.pipe(Effect.catch(() => Effect.succeed("")))
if (gitignore) ignored.add(gitignore)
const ignorefile = yield* raw
.readFileString(path.join(location.project.directory, ".ignore"))
.pipe(Effect.catch(() => Effect.succeed("")))
if (ignorefile) ignored.add(ignorefile)
return (yield* fs.list({ path: RelativePath.make(ctx.query.path) })).map((item) => ({
name: path.basename(item.path),
path: item.path,
absolute: path.join(directory, item.path),
type: item.type,
ignored: ignored.ignores(
path.relative(location.project.directory, path.join(location.directory, item.path)) +
(item.type === "directory" ? "/" : ""),
),
),
),
}))
}),
)
})
@ -103,9 +93,9 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
FileSystem.Service.use((fs) => fs.read({ path: RelativePath.make(ctx.query.path) })),
).pipe(
Effect.map((item) => ({
type: item.type,
content: item.type === "text" ? item.content.trim() : item.content,
...(item.type === "binary" ? { encoding: item.encoding, mimeType: item.mime } : {}),
type: item.encoding === "utf8" ? ("text" as const) : ("binary" as const),
content: item.encoding === "utf8" ? item.content.trim() : item.content,
...(item.encoding === "base64" ? { encoding: item.encoding, mimeType: item.mime } : {}),
})),
)
})

View file

@ -29,8 +29,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import * as DateTime from "effect/DateTime"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { toolFileSourceFromUri, Usage, type LLMEvent } from "@opencode-ai/llm"
import { ToolOutput } from "@opencode-ai/core/tool-output"
import { ToolOutput, Usage, type LLMEvent } from "@opencode-ai/llm"
const DOOM_LOOP_THRESHOLD = 3
export type Result = "compact" | "stop" | "continue"
@ -595,20 +594,20 @@ export const layer = Layer.effect(
if (mirrorAssistant) {
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
const content = [
ToolOutput.text({ type: "text", text: output.output }),
{ type: "text" as const, text: output.output },
...(output.attachments?.map((item: SessionV1.FilePart) =>
ToolOutput.file({
({
type: "file",
source: toolFileSourceFromUri(item.url),
uri: item.url,
mime: item.mime,
name: item.filename,
}),
}) as const,
) ?? []),
]
const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
const unsupported = content.find((item) => item.type === "file" && !item.uri.startsWith("data:"))
if (unsupported?.type === "file") {
const error = new Error(
`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`,
`Tool attachment URI "${unsupported.uri}" must be materialized before durable V2 settlement`,
)
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: ctx.sessionID,

View file

@ -670,6 +670,11 @@ const scenarios: Scenario[] = [
.at((ctx) => ({ path: "/api/fs/read?path=hello.txt", headers: ctx.headers() }))
.json(200, locationData(object)),
http.protected.get("/api/fs/list", "v2.fs.list").json(200, locationData(array)),
http.protected
.get("/api/fs/find", "v2.fs.find")
.seeded((ctx) => ctx.file("hello.txt", "hello\n"))
.at((ctx) => ({ path: "/api/fs/find?query=hello&type=file", headers: ctx.headers() }))
.json(200, locationData(array)),
http.protected.get("/api/reference", "v2.reference.list").json(200, object),
http.protected
.get("/api/provider/{providerID}", "v2.provider.get")

View file

@ -390,10 +390,13 @@ describe("HttpApi SDK", () => {
onRequest: (value) => (request = value),
})
const file = yield* call(() => sdk.v2.fs.read({ path: "hello.txt" }))
const found = yield* call(() => sdk.v2.fs.find({ query: "hello", type: "file" }))
const url = new URL(request!.url)
expect(file.response.status).toBe(200)
expect(file.data).toMatchObject({ data: { content: "hello" } })
expect(found.response.status).toBe(200)
expect(found.data).toMatchObject({ data: [{ path: "hello.txt", type: "file" }] })
expect(url.searchParams.get("directory")).toBe(directory)
expect(url.searchParams.get("workspace")).toBe(workspaceID)
expect(url.searchParams.get("location[directory]")).toBe(directory)

View file

@ -8,7 +8,6 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ToolOutput } from "@opencode-ai/core/tool-output"
test.skip("step snapshots carry over to assistant messages", () => {
const state: SessionMessageUpdater.MemoryState = { messages: [] }
@ -182,7 +181,7 @@ test.skip("tool completion stores completed timestamp", () => {
timestamp: DateTime.makeUnsafe(4),
callID,
structured: {},
content: [ToolOutput.text({ type: "text", text: "/tmp" })],
content: [{ type: "text", text: "/tmp" }],
provider: { executed: true, metadata: { fake: { status: "done" } } },
},
} satisfies SessionEvent.Event),