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

This commit is contained in:
Dax
2026-06-09 14:28:45 -04:00
committed by GitHub
parent b750fb79b6
commit 6cecc3d03e
73 changed files with 1054 additions and 1422 deletions
+6 -2
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)
}),
})
@@ -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 } : {}),
})),
)
})
+7 -8
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,