refactor(core): consolidate references

This commit is contained in:
Dax Raad 2026-06-09 11:38:35 -04:00
commit f76ca58a54
63 changed files with 683 additions and 2722 deletions

View file

@ -7,25 +7,14 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { Repository } from "@opencode-ai/core/repository"
import { Global } from "@opencode-ai/core/global"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { tmpdir } from "./fixture/tmpdir"
import { location } from "./fixture/location"
import { it } from "./lib/effect"
const inertReferences = ProjectReference.Service.of({
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
resolveMention: () => Effect.succeed(undefined),
ensurePath: () => Effect.void,
containsManagedPath: () => Effect.succeed(false),
})
function provide(
directory: string,
references = inertReferences,
filesystem = FSUtil.defaultLayer,
data = Global.Path.data,
) {
@ -36,7 +25,6 @@ function provide(
filesystem,
Ripgrep.defaultLayer,
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
Layer.succeed(ProjectReference.Service, references),
Global.layerWith({ data }),
),
),
@ -69,7 +57,7 @@ describe("FileSystem", () => {
expect((yield* service.resolveRoot({ path: output })).real).toBe(output)
expect(yield* Effect.exit(service.read({ path: unrelated }))).toMatchObject({ _tag: "Failure" })
expect(yield* Effect.exit(service.read({ path: managed }))).toMatchObject({ _tag: "Failure" })
}).pipe(provide(worktree, inertReferences, FSUtil.defaultLayer, data))
}).pipe(provide(worktree, FSUtil.defaultLayer, data))
}),
)
@ -258,7 +246,7 @@ describe("FileSystem", () => {
}
yield* Effect.promise(() => fs.rename(text, text + ".moved"))
yield* Effect.promise(() => fs.rename(binary, binary + ".moved"))
}).pipe(provide(directory, inertReferences, filesystem))
}).pipe(provide(directory, filesystem))
}),
)
@ -344,7 +332,7 @@ describe("FileSystem", () => {
next: 3,
})
expect(realPaths.filter((target) => target !== directory)).toEqual([path.join(directory, "alpha.txt")])
}).pipe(provide(directory, inertReferences, filesystem))
}).pipe(provide(directory, filesystem))
}),
)
@ -380,7 +368,7 @@ describe("FileSystem", () => {
expect((yield* service.listPage({ limit: 32 })).entries).toHaveLength(32)
expect(maximum).toBe(16)
}).pipe(provide(directory, inertReferences, filesystem))
}).pipe(provide(directory, filesystem))
}),
)
@ -402,9 +390,8 @@ describe("FileSystem", () => {
),
)
test("rejects empty list aliases and page limits over 2000", () => {
test("rejects page limits over 2000", () => {
const decode = Schema.decodeUnknownSync(FileSystem.ListPageInput)
expect(() => decode({ reference: "" })).toThrow()
expect(() => decode({ limit: 2_001 })).toThrow()
})
@ -461,125 +448,4 @@ describe("FileSystem", () => {
),
)
it.live("reads and lists paths relative to a local project reference", () =>
withTmp((directory) => {
const docs = path.join(directory, "docs")
return Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.mkdir(docs)
await fs.writeFile(path.join(docs, "README.md"), "docs")
})
const service = yield* FileSystem.Service
expect(yield* service.read({ reference: "docs", path: RelativePath.make("README.md") })).toMatchObject({
type: "text",
content: "docs",
})
expect(yield* service.list({ reference: "docs" })).toMatchObject([{ path: "README.md", type: "file" }])
}).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } })))
}),
)
it.live("materializes Git references before filesystem access", () =>
withTmp((directory) => {
const docs = path.join(directory, "docs")
const ensured: string[] = []
return Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.mkdir(docs)
await fs.writeFile(path.join(docs, "README.md"), "docs")
})
expect(
yield* (yield* FileSystem.Service).read({ reference: "sdk", path: RelativePath.make("README.md") }),
).toMatchObject({ content: "docs" })
expect(ensured).toEqual([docs])
}).pipe(
provide(
directory,
references(
{
sdk: {
name: "sdk",
kind: "git",
repository: "owner/repo",
reference: Repository.parseRemote("owner/repo"),
path: docs,
},
},
(target) => Effect.sync(() => ensured.push(target ?? "")),
),
),
)
}),
)
it.live("rejects unknown, invalid, and escaping project reference paths", () =>
withTmp((directory) => {
const docs = path.join(directory, "docs")
return Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(docs))
const service = yield* FileSystem.Service
expect(Exit.isFailure(yield* service.list({ reference: "unknown" }).pipe(Effect.exit))).toBe(true)
expect(Exit.isFailure(yield* service.list({ reference: "invalid" }).pipe(Effect.exit))).toBe(true)
expect(
Exit.isFailure(
yield* service.read({ reference: "docs", path: RelativePath.make("../outside") }).pipe(Effect.exit),
),
).toBe(true)
}).pipe(
provide(
directory,
references({
docs: { name: "docs", kind: "local", path: docs },
invalid: { name: "invalid", kind: "invalid", message: "invalid reference" },
}),
),
)
}),
)
it.live("rejects aliases when project references are disabled", () =>
withTmp((directory) =>
Effect.gen(function* () {
expect(Exit.isFailure(yield* (yield* FileSystem.Service).list({ reference: "docs" }).pipe(Effect.exit))).toBe(
true,
)
}).pipe(provide(directory)),
),
)
it.live("rejects symlink escapes from project references", () =>
withTmp((directory) => {
const docs = path.join(directory, "docs")
const outside = path.join(directory, "outside.txt")
return Effect.gen(function* () {
if (process.platform === "win32") return
yield* Effect.promise(async () => {
await fs.mkdir(docs)
await fs.writeFile(outside, "outside")
await fs.symlink(outside, path.join(docs, "link.txt"))
})
expect(
Exit.isFailure(
yield* (yield* FileSystem.Service)
.read({ reference: "docs", path: RelativePath.make("link.txt") })
.pipe(Effect.exit),
),
).toBe(true)
}).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } })))
}),
)
})
function references(
entries: Record<string, ProjectReference.Resolved>,
ensurePath: ProjectReference.Interface["ensurePath"] = () => Effect.void,
) {
return ProjectReference.Service.of({
list: () => Effect.succeed(Object.values(entries)),
get: (name) => Effect.succeed(entries[name]),
resolveMention: () => Effect.succeed(undefined),
ensurePath,
containsManagedPath: () => Effect.succeed(false),
})
}

View file

@ -18,7 +18,7 @@ import { Global } from "../src/global"
import { ModelsDev } from "../src/models-dev"
import { Npm } from "../src/npm"
import { Project } from "../src/project"
import { ProjectReference } from "../src/project-reference"
import { Reference } from "../src/reference"
import { LocationSearch } from "../src/location-search"
import { ToolRegistry } from "../src/tool/registry"
import { ApplicationTools } from "../src/tool/application-tools"
@ -71,7 +71,7 @@ describe("LocationServiceMap", () => {
const update = (directory: string) =>
Effect.gen(function* () {
yield* PluginBoot.Service.use((boot) => boot.wait())
yield* ProjectReference.Service
yield* Reference.Service
yield* LocationSearch.Service
const catalog = yield* Catalog.Service
const transform = yield* catalog.transform()

View file

@ -171,7 +171,7 @@ describe("LocationMutation", () => {
),
)
test("keeps project references outside the mutation input API", () => {
test("ignores unknown mutation input fields", () => {
expect(Object.keys(LocationMutation.ResolveInput.fields)).toEqual(["path", "kind"])
expect(Schema.decodeUnknownSync(LocationMutation.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
path: "README.md",

View file

@ -8,7 +8,6 @@ import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationSearch } from "@opencode-ai/core/location-search"
import { AppProcess } from "@opencode-ai/core/process"
import { Ripgrep as FileSystemRipgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
@ -16,15 +15,12 @@ import { tmpdir } from "./fixture/tmpdir"
import { location } from "./fixture/location"
import { it } from "./lib/effect"
const inertReferences = references({})
function provide(directory: string, projectReferences = inertReferences, data = Global.Path.data) {
function provide(directory: string, data = Global.Path.data) {
const dependencies = Layer.mergeAll(
FSUtil.defaultLayer,
FileSystemRipgrep.defaultLayer,
AppProcess.defaultLayer,
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
Layer.succeed(ProjectReference.Service, projectReferences),
Global.layerWith({ data }),
)
const filesystem = FileSystem.layer.pipe(Layer.provide(dependencies))
@ -56,7 +52,7 @@ describe("LocationSearch", () => {
const search = yield* LocationSearch.Service
const result = yield* search.grep({ pattern: "FAIL", path: output })
expect(result.items).toMatchObject([{ canonical: output, line: 2, lines: "FAIL here\n" }])
}).pipe(provide(directory, inertReferences, data))
}).pipe(provide(directory, data))
}),
)
@ -83,7 +79,7 @@ describe("LocationSearch", () => {
),
)
it.live("searches files under a relative subdirectory and named local reference", () =>
it.live("searches files under a relative subdirectory", () =>
withTmp((directory) => {
const docs = path.join(directory, "docs")
return Effect.gen(function* () {
@ -98,11 +94,7 @@ describe("LocationSearch", () => {
expect(
(yield* search.files({ pattern: "*.ts", path: RelativePath.make("src") })).items.map((item) => item.path),
).toEqual([RelativePath.make("src/active.ts")])
const guide = yield* Effect.promise(() => fs.realpath(path.join(docs, "guide.md")))
expect((yield* search.files({ pattern: "*.md", reference: "docs" })).items).toMatchObject([
{ path: RelativePath.make("guide.md"), resource: "docs:guide.md", canonical: guide },
])
}).pipe(provide(directory, references({ docs: { name: "docs", kind: "local", path: docs } })))
}).pipe(provide(directory))
}),
)
@ -264,13 +256,3 @@ describe("LocationSearch", () => {
expect(() => decode({ pattern: "*", limit: LocationSearch.MAX_RESULT_LIMIT + 1 })).toThrow()
})
})
function references(entries: Record<string, ProjectReference.Resolved>) {
return ProjectReference.Service.of({
list: () => Effect.succeed(Object.values(entries)),
get: (name) => Effect.succeed(entries[name]),
resolveMention: () => Effect.succeed(undefined),
ensurePath: () => Effect.void,
containsManagedPath: () => Effect.succeed(false),
})
}

View file

@ -1,299 +0,0 @@
import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Deferred, Effect, Layer, Schema } from "effect"
import { Config } from "@opencode-ai/core/config"
import { ConfigReference } from "@opencode-ai/core/config/reference"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Location } from "@opencode-ai/core/location"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { Repository } from "@opencode-ai/core/repository"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
describe("ProjectReference", () => {
it.live("uses the broad experimental flag unless references are explicitly configured", () =>
withEnv(
{ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: undefined },
Effect.sync(() => {
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(true)
}),
).pipe(
Effect.flatMap(() =>
withEnv(
{ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: "false" },
Effect.sync(() => {
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(false)
}),
),
),
),
)
it.live("normalizes aliases and resolves relative local paths from the project root", () =>
withTmp((tmp) =>
Effect.gen(function* () {
const project = path.join(tmp.path, "project")
const nested = path.join(project, "packages", "app")
yield* Effect.promise(() => fs.mkdir(nested, { recursive: true }))
const references = ProjectReference.resolveAll({
references: ConfigReference.normalize({
docs: { path: "./docs" },
home: "~/notes",
sdk: { repository: "owner/repo", branch: "main" },
shorthand: "owner/other",
invalid: "not-a-repo",
"bad/name": "owner/repo",
}),
directory: project,
home: path.join(tmp.path, "home"),
repos: path.join(tmp.path, "repos"),
})
expect(references).toMatchObject([
{ name: "docs", kind: "local", path: path.join(project, "docs") },
{ name: "home", kind: "local", path: path.join(tmp.path, "home", "notes") },
{ name: "sdk", kind: "git", branch: "main" },
{ name: "shorthand", kind: "git" },
{ name: "invalid", kind: "invalid", repository: "not-a-repo" },
{ name: "bad/name", kind: "invalid" },
])
}),
),
)
it.live("marks same-cache references with different branches invalid", () =>
Effect.sync(() => {
const references = ProjectReference.resolveAll({
references: ConfigReference.normalize({
main: { repository: "owner/repo", branch: "main" },
dev: { repository: "github.com/owner/repo", branch: "dev" },
alsoMain: { repository: "https://github.com/owner/repo", branch: "main" },
}),
directory: "/project",
home: "/home",
repos: "/repos",
})
expect(references.map((reference) => reference.kind)).toEqual(["git", "invalid", "git"])
expect(references[1]?.kind === "invalid" ? references[1].message : "").toContain("conflicts with @main")
}),
)
it.live("merges config aliases and exposes mention and managed-path operations", () =>
withoutReferences(
withTmp((tmp) => {
const calls: RepositoryCache.EnsureInput[] = []
const project = path.join(tmp.path, "project")
const nested = path.join(project, "packages", "app")
const docs = path.join(project, "docs")
const repos = path.join(tmp.path, "repos")
return Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.mkdir(nested, { recursive: true })
await fs.mkdir(docs)
await fs.writeFile(path.join(docs, "README.md"), "docs")
})
yield* withReferences(
Effect.gen(function* () {
const references = yield* ProjectReference.Service
const git = path.join(repos, "github.com", "owner", "repo")
expect(yield* references.list()).toMatchObject([
{ name: "docs", kind: "local", path: docs },
{ name: "sdk", kind: "git", path: git },
])
expect(yield* references.resolveMention("docs/README.md")).toMatchObject({
name: "docs",
kind: "reference",
target: "README.md",
path: path.join(docs, "README.md"),
})
expect(yield* references.resolveMention("docs/missing.md")).toMatchObject({
name: "docs",
kind: "missing",
})
expect(yield* references.resolveMention("docs/../outside.md")).toMatchObject({
name: "docs",
kind: "invalid",
})
expect(yield* references.resolveMention("unknown")).toBeUndefined()
expect(yield* references.resolveMention("sdk")).toMatchObject({
name: "sdk",
kind: "reference",
path: git,
})
expect(yield* references.containsManagedPath(path.join(git, "README.md"))).toBe(true)
expect(yield* references.containsManagedPath(path.join(docs, "README.md"))).toBe(false)
yield* references.ensurePath()
expect(calls).toHaveLength(1)
}).pipe(
Effect.provide(
testLayer({
directory: nested,
project,
repos,
documents: [
document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
document({ docs: { path: "./docs" }, sdk: { repository: "owner/repo", branch: "main" } }),
],
ensure: (input) => Effect.sync(() => result(repos, calls, input)),
}),
),
),
)
})
}),
),
)
it.live("is inert while the runtime flag is disabled", () =>
withoutReferences(
withTmp((tmp) => {
const calls: RepositoryCache.EnsureInput[] = []
return Effect.gen(function* () {
const references = yield* ProjectReference.Service
expect(yield* references.list()).toEqual([])
expect(yield* references.get("sdk")).toBeUndefined()
expect(yield* references.resolveMention("sdk")).toBeUndefined()
expect(
yield* references.containsManagedPath(path.join(tmp.path, "repos", "github.com", "owner", "repo")),
).toBe(false)
yield* references.ensurePath()
expect(calls).toEqual([])
}).pipe(
Effect.provide(
testLayer({
directory: tmp.path,
project: tmp.path,
repos: path.join(tmp.path, "repos"),
documents: [document({ sdk: "owner/repo" })],
ensure: (input) => Effect.sync(() => result(path.join(tmp.path, "repos"), calls, input)),
}),
),
)
}),
),
)
it.live("starts Git materialization in the background without blocking the location layer", () =>
withTmp((tmp) =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>()
yield* withReferences(
Effect.gen(function* () {
expect(yield* (yield* ProjectReference.Service).list()).toHaveLength(1)
yield* Deferred.await(started).pipe(
Effect.timeoutOrElse({
duration: "1 second",
orElse: () => Effect.die(new Error("refresh did not start")),
}),
)
}).pipe(
Effect.provide(
testLayer({
directory: tmp.path,
project: tmp.path,
repos: path.join(tmp.path, "repos"),
documents: [document({ sdk: "owner/repo" })],
ensure: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
}),
),
),
)
}),
),
)
})
function document(references: ConfigReference.Info) {
return new Config.Document({ type: "document", info: Schema.decodeUnknownSync(Config.Info)({ references }) })
}
function result(
repos: string,
calls: RepositoryCache.EnsureInput[],
input: RepositoryCache.EnsureInput,
): RepositoryCache.Result {
calls.push(input)
return {
repository: input.reference.label,
host: input.reference.host,
remote: input.reference.remote,
localPath: Repository.cachePath(repos, input.reference),
status: "cached",
branch: input.branch,
}
}
function testLayer(input: {
directory: string
project: string
repos: string
documents: Config.Document[]
ensure: RepositoryCache.Interface["ensure"]
}) {
return ProjectReference.layer.pipe(
Layer.provide(
Layer.mergeAll(
FSUtil.defaultLayer,
Global.layerWith({ home: path.join(input.directory, "home"), repos: input.repos }),
Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(input.directory) },
{ projectDirectory: AbsolutePath.make(input.project) },
),
),
),
Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed(input.documents) })),
Layer.succeed(RepositoryCache.Service, RepositoryCache.Service.of({ ensure: input.ensure })),
),
),
)
}
function withTmp<A, E, R>(body: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
body,
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
}
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
return withEnv({ OPENCODE_EXPERIMENTAL_REFERENCES: "true" }, body)
}
function withoutReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
return withEnv({ OPENCODE_EXPERIMENTAL: undefined, OPENCODE_EXPERIMENTAL_REFERENCES: undefined }, body)
}
function withEnv<A, E, R>(env: Record<string, string | undefined>, body: Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => {
const previous = Object.fromEntries(Object.keys(env).map((key) => [key, process.env[key]]))
for (const [key, value] of Object.entries(env)) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
return previous
}),
() => body,
(previous) =>
Effect.sync(() => {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
}),
)
}

View file

@ -0,0 +1,57 @@
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer, Scope } from "effect"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
import { Reference } from "@opencode-ai/core/reference"
import { Repository } from "@opencode-ai/core/repository"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { EventV2 } from "@opencode-ai/core/event"
import { it } from "./lib/effect"
const cache = Layer.mock(RepositoryCache.Service, {
ensure: () => Effect.die("unexpected Git materialization"),
})
describe("Reference", () => {
it.effect("registers normalized sources for the owning scope", () =>
Effect.gen(function* () {
const references = yield* Reference.Service
const scope = yield* Scope.make()
const update = yield* references.transform().pipe(Effect.provideService(Scope.Scope, scope))
const path = AbsolutePath.make("/docs")
yield* update((editor) => editor.add("docs", new Reference.LocalSource({ type: "local", path })))
expect(yield* references.list()).toEqual([
new Reference.Info({ name: "docs", path, source: new Reference.LocalSource({ type: "local", path }) }),
])
yield* Scope.close(scope, Exit.void)
expect(yield* references.list()).toEqual([])
}).pipe(
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
)
it.effect("derives Git paths without exposing cache operations", () =>
Effect.gen(function* () {
const references = yield* Reference.Service
const update = yield* references.transform()
const repository = Repository.parseRemote("owner/repo")
const source = new Reference.GitSource({ type: "git", repository: "owner/repo", branch: "main" })
yield* update((editor) => editor.add("sdk", source))
expect(yield* references.list()).toEqual([
new Reference.Info({ name: "sdk", path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), source }),
])
}).pipe(
Effect.scoped,
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
)
})

View file

@ -4,7 +4,7 @@ import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { AgentAttachment, FileAttachment, ReferenceAttachment } from "@opencode-ai/core/session/prompt"
import { AgentAttachment, FileAttachment } from "@opencode-ai/core/session/prompt"
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolOutput } from "@opencode-ai/core/tool-output"
@ -17,7 +17,6 @@ const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.
describe("toLLMMessages", () => {
test("maps every top-level V2 Session message type", () => {
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" })
const messages = toLLMMessages(
[
new SessionMessage.AgentSwitched({
@ -44,7 +43,6 @@ describe("toLLMMessages", () => {
text: "Inspect this image",
files: [file],
agents: [new AgentAttachment({ name: "build" })],
references: [reference],
time: { created },
}),
new SessionMessage.Synthetic({
@ -84,7 +82,7 @@ describe("toLLMMessages", () => {
{ type: "text", text: "Inspect this image" },
{ type: "media", mediaType: "image/png", data: "data:image/png;base64,aGVsbG8=", filename: "hello.png" },
],
metadata: { agents: [{ name: "build" }], references: [reference] },
metadata: { agents: [{ name: "build" }] },
}),
)
expect(messages.slice(2).map((message) => message.content)).toEqual([

View file

@ -43,12 +43,10 @@ const filesystem = Layer.succeed(
Effect.sync(() => {
resolutions.push(input)
const relative = input.path ?? RelativePath.make(".")
const resource = input.reference === undefined ? relative : `${input.reference}:${relative}`
return new FileSystem.RootTarget({
real: `/project/${relative}`,
root: "/project",
resource,
reference: input.reference,
resource: relative,
type: "directory",
})
}),
@ -122,10 +120,10 @@ describe("GlobTool", () => {
action: "glob",
resources: ["**/*.ts"],
save: ["*"],
metadata: { root: "src", reference: undefined, path: "src", limit: 12 },
metadata: { root: "src", path: "src", limit: 12 },
},
])
expect(resolutions).toEqual([{ path: RelativePath.make("src"), reference: undefined }])
expect(resolutions).toEqual([{ path: RelativePath.make("src") }])
expect(searches).toEqual([{ pattern: "**/*.ts", path: RelativePath.make("src"), limit: 12 }])
}),
)
@ -169,39 +167,6 @@ describe("GlobTool", () => {
}),
)
it.effect("searches named references with root and reference metadata", () =>
Effect.gen(function* () {
reset()
result = new LocationSearch.FilesResult({
items: [
new LocationSearch.File({
path: RelativePath.make("guide.md"),
canonical: "/project/docs/guide.md",
resource: "docs:guide.md",
mtime: 1,
}),
],
truncated: false,
partial: false,
})
expect(yield* executeTool(yield* ToolRegistry.Service, call({ pattern: "*.md", reference: "docs" }))).toEqual({
type: "text",
value: "docs:guide.md",
})
expect(assertions).toMatchObject([
{
sessionID,
action: "glob",
resources: ["*.md"],
save: ["*"],
metadata: { root: "docs:.", reference: "docs", path: undefined, limit: undefined },
},
])
expect(searches).toEqual([{ pattern: "*.md", reference: "docs" }])
}),
)
it.effect("formats bounded and partial results without discarding structured output", () =>
Effect.sync(() => {
const output = new LocationSearch.FilesResult({

View file

@ -9,7 +9,6 @@ import { Ripgrep as FileSystemRipgrep } from "@opencode-ai/core/filesystem/ripgr
import { LocationSearch } from "@opencode-ai/core/location-search"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AppProcess } from "@opencode-ai/core/process"
import { ProjectReference } from "@opencode-ai/core/project-reference"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
@ -39,8 +38,7 @@ const filesystem = Layer.succeed(
new FileSystem.RootTarget({
real: `/project/${input.path ?? "."}`,
root: "/project",
resource: input.reference === undefined ? (input.path ?? ".") : `${input.reference}:${input.path ?? "."}`,
reference: input.reference,
resource: input.path ?? ".",
type: "directory",
}),
),
@ -115,23 +113,12 @@ const reset = () => {
result = new LocationSearch.GrepResult({ items: [], truncated: false, partial: false })
}
function references(entries: Record<string, ProjectReference.Resolved>) {
return ProjectReference.Service.of({
list: () => Effect.succeed(Object.values(entries)),
get: (name) => Effect.succeed(entries[name]),
resolveMention: () => Effect.succeed(undefined),
ensurePath: () => Effect.void,
containsManagedPath: () => Effect.succeed(false),
})
}
function provideLive(directory: string, projectReferences = references({})) {
function provideLive(directory: string) {
const dependencies = Layer.mergeAll(
FSUtil.defaultLayer,
FileSystemRipgrep.defaultLayer,
AppProcess.defaultLayer,
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
Layer.succeed(ProjectReference.Service, projectReferences),
)
const filesystem = FileSystem.layer.pipe(Layer.provide(dependencies))
const search = LocationSearch.layer.pipe(
@ -170,29 +157,13 @@ describe("GrepTool", () => {
action: "grep",
resources: ["needle"],
save: ["*"],
metadata: { root: "src", reference: undefined, path: RelativePath.make("src"), include: "*.ts", limit: 2 },
metadata: { root: "src", path: RelativePath.make("src"), include: "*.ts", limit: 2 },
},
])
expect(searches).toEqual([{ pattern: "needle", path: RelativePath.make("src"), include: "*.ts", limit: 2 }])
}),
)
it.effect("delegates named reference grep and exposes the canonical selected root in metadata", () =>
Effect.gen(function* () {
reset()
yield* execute({ pattern: "guide", path: "docs", reference: "manual", include: "*.md" })
expect(assertions[0]).toMatchObject({
resources: ["guide"],
metadata: { root: "manual:docs", reference: "manual", path: RelativePath.make("docs"), include: "*.md" },
})
expect(searches).toEqual([
{ pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" },
])
}),
)
it.effect("does not search when permission is denied", () =>
Effect.gen(function* () {
reset()
@ -248,7 +219,7 @@ describe("GrepTool", () => {
}),
)
runtimeIt.live("greps active Location and named-reference files with include globs", () =>
runtimeIt.live("greps active Location files with include globs", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
@ -259,23 +230,15 @@ describe("GrepTool", () => {
reset()
yield* Effect.promise(async () => {
await fs.mkdir(path.join(tmp.path, "src"))
await fs.mkdir(docs)
await fs.writeFile(path.join(tmp.path, "src", "index.ts"), "needle ts\n")
await fs.writeFile(path.join(tmp.path, "src", "notes.txt"), "needle txt\n")
await fs.writeFile(path.join(docs, "guide.md"), "needle docs\n")
})
expect(yield* execute({ pattern: "needle", path: "src", include: "*.ts" })).toEqual({
type: "text",
value: "Found 1 matches\nsrc/index.ts:\n Line 1: needle ts\n",
})
expect(yield* execute({ pattern: "needle", reference: "docs", include: "*.md" })).toEqual({
type: "text",
value: "Found 1 matches\ndocs:guide.md:\n Line 1: needle docs\n",
})
}).pipe(
Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } }))),
)
}).pipe(Effect.provide(provideLive(tmp.path)))
}),
),
)

View file

@ -35,7 +35,7 @@ const filesystem = Layer.succeed(
? Effect.succeed(
new FileSystem.ReadPath({
type: resolvedType,
resource: input.reference === undefined ? input.path : `${input.reference}:${input.path}`,
resource: input.path,
}),
)
: Effect.die(resolveFailure),
@ -457,20 +457,6 @@ describe("ReadTool", () => {
}),
)
it.effect("authorizes project references with their canonical identity", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md", reference: "docs" } },
})
expect(assertions).toMatchObject([{ resources: ["docs:README.md"] }])
}),
)
it.effect("preserves unexpected resolution defects", () =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service