refactor(core): consolidate references (#31539)

This commit is contained in:
Dax
2026-06-09 12:08:58 -04:00
committed by GitHub
parent 0bb677cef9
commit 6566ede935
65 changed files with 687 additions and 2730 deletions
-48
View File
@@ -1,48 +0,0 @@
export * as ConfigReference from "./reference"
import { ConfigReferenceV1 } from "@opencode-ai/core/v1/config/reference"
export type NormalizedEntry =
| {
kind: "local"
path: string
}
| {
kind: "git"
repository: string
branch?: string
}
| {
kind: "invalid"
message: string
}
export type NormalizedInfo = Record<string, NormalizedEntry>
export function validateAlias(name: string) {
if (name.length === 0) return "Reference alias must not be empty"
if (/[\/\s`,]/.test(name)) {
return "Reference alias must not contain /, whitespace, comma, or backtick"
}
}
export function normalizeEntry(entry: ConfigReferenceV1.Entry): NormalizedEntry {
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
return { kind: "local", path: entry }
}
return { kind: "git", repository: entry }
}
if ("path" in entry) return { kind: "local", path: entry.path }
return { kind: "git", repository: entry.repository, branch: entry.branch }
}
export function normalize(info: ConfigReferenceV1.Info): NormalizedInfo {
return Object.fromEntries(
Object.entries(info).map(([name, entry]) => {
const aliasError = validateAlias(name)
return [name, aliasError ? { kind: "invalid" as const, message: aliasError } : normalizeEntry(entry)] as const
}),
)
}
@@ -42,7 +42,6 @@ import { Format } from "@/format"
import { InstanceLayer } from "@/project/instance-layer"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Reference } from "@/reference/reference"
import { Workspace } from "@/control-plane/workspace"
import { Worktree } from "@/worktree"
import { Installation } from "@/installation"
@@ -98,7 +97,6 @@ export const AppLayer = Layer.mergeAll(
Format.defaultLayer,
Project.defaultLayer,
Vcs.defaultLayer,
Reference.defaultLayer,
Workspace.defaultLayer,
Worktree.appLayer,
Installation.defaultLayer,
+1 -4
View File
@@ -11,7 +11,6 @@ import { Search } from "@opencode-ai/core/filesystem/search"
import { Effect, Layer } from "effect"
import { Config } from "@/config/config"
import { Service } from "./bootstrap-service"
import { Reference } from "@/reference/reference"
export { Service } from "./bootstrap-service"
export type { Interface } from "./bootstrap-service"
@@ -27,7 +26,6 @@ export const layer = Layer.effect(
const lsp = yield* LSP.Service
const plugin = yield* Plugin.Service
const project = yield* Project.Service
const reference = yield* Reference.Service
const search = yield* Search.Service
const shareNext = yield* ShareNext.Service
const snapshot = yield* Snapshot.Service
@@ -52,7 +50,7 @@ export const layer = Layer.effect(
// Each service self-manages its own slow work via Effect.forkScoped against
// its per-instance state scope. We just await materialization here.
yield* Effect.forEach(
[reference, lsp, shareNext, format, vcs, snapshot, project],
[lsp, shareNext, format, vcs, snapshot, project],
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
{ concurrency: "unbounded", discard: true },
).pipe(Effect.withSpan("InstanceBootstrap.init"))
@@ -69,7 +67,6 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
LSP.defaultLayer,
Plugin.defaultLayer,
Project.defaultLayer,
Reference.defaultLayer,
Search.defaultLayer,
ShareNext.defaultLayer,
Snapshot.defaultLayer,
@@ -1,237 +0,0 @@
import path from "path"
import { Effect, Context, Layer, Scope } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
import { Config } from "@/config/config"
import { ConfigReference } from "@/config/reference"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { parseRepositoryReference, repositoryCachePath, type RemoteReference } from "@/util/repository"
import { RepositoryCache } from "./repository-cache"
export type Resolved =
| {
name: string
kind: "local"
path: string
}
| {
name: string
kind: "git"
repository: string
reference: RemoteReference
path: string
branch?: string
}
| {
name: string
kind: "invalid"
repository?: string
message: string
}
type State = {
references: Resolved[]
materializeAll: Effect.Effect<void>
materializeByPath: Materializer[]
}
type Materializer = { path: string; run: Effect.Effect<void> }
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly list: () => Effect.Effect<Resolved[]>
readonly get: (name: string) => Effect.Effect<Resolved | undefined>
readonly ensure: (target?: string) => Effect.Effect<void>
readonly contains: (target?: string) => Effect.Effect<boolean>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Reference") {}
export function referencePath(input: { directory: string; worktree: string; value: string }) {
if (input.value.startsWith("~/")) return path.join(Global.Path.home, input.value.slice(2))
return path.isAbsolute(input.value)
? input.value
: path.resolve(input.worktree === "/" ? input.directory : input.worktree, input.value)
}
function resolveGit(
input: { name: string; repository: string } | { name: string; repository: string; branch: string | undefined },
): Resolved {
const parsed = parseRepositoryReference(input.repository)
if (!parsed || parsed.protocol === "file:") {
return {
name: input.name,
kind: "invalid",
repository: input.repository,
message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
}
}
return {
name: input.name,
kind: "git",
repository: input.repository,
reference: parsed,
path: repositoryCachePath(parsed),
...("branch" in input ? { branch: input.branch } : {}),
}
}
function branchLabel(branch: string | undefined) {
return branch ?? "default branch"
}
function normalizedTarget(target?: string) {
if (!target) return
return process.platform === "win32" ? FSUtil.normalizePath(target) : target
}
function containsReferencePath(referencePath: string, target: string) {
return FSUtil.contains(normalizedTarget(referencePath) ?? referencePath, target)
}
function uniqueGitReferences(references: Resolved[]) {
const seenPath = new Set<string>()
return references.filter((reference): reference is Extract<Resolved, { kind: "git" }> => {
if (reference.kind !== "git") return false
if (seenPath.has(reference.path)) return false
seenPath.add(reference.path)
return true
})
}
function materializeReference(cache: RepositoryCache.Interface, reference: Extract<Resolved, { kind: "git" }>) {
return cache.ensure({ reference: reference.reference, branch: reference.branch, refresh: true }).pipe(
Effect.asVoid,
Effect.catchCause((cause) =>
Effect.logWarning("failed to materialize reference repository", { name: reference.name, cause }),
),
)
}
const materializers = Effect.fn("Reference.materializers")(function* (
cache: RepositoryCache.Interface,
references: Resolved[],
) {
return yield* Effect.forEach(
uniqueGitReferences(references),
Effect.fnUntraced(function* (reference) {
return { path: reference.path, run: yield* Effect.cached(materializeReference(cache, reference)) }
}),
{ concurrency: "unbounded" },
)
})
function materializeAll(input: { flags: RuntimeFlags.Info; materializers: Materializer[] }) {
if (!input.flags.experimentalReferences) return Effect.void
return Effect.forEach(
input.materializers,
Effect.fnUntraced(function* (item) {
yield* item.run
}),
{ concurrency: 4, discard: true },
)
}
function materializeByPath(materializers: Materializer[], target: string) {
return materializers.find((item) => containsReferencePath(item.path, target))?.run ?? Effect.void
}
function containsGitReferencePath(references: Resolved[], target: string) {
return references.some((reference) => reference.kind === "git" && containsReferencePath(reference.path, target))
}
export function resolve(input: {
name: string
reference: ConfigReference.NormalizedEntry
directory: string
worktree: string
}): Resolved {
if (input.reference.kind === "invalid") {
return { name: input.name, kind: "invalid", message: input.reference.message }
}
if (input.reference.kind === "local") {
return { name: input.name, kind: "local", path: referencePath({ ...input, value: input.reference.path }) }
}
return resolveGit({ name: input.name, repository: input.reference.repository, branch: input.reference.branch })
}
export function resolveAll(input: { references: ConfigReference.NormalizedInfo; directory: string; worktree: string }) {
const seen = new Map<string, { name: string; branch?: string }>()
return Object.entries(input.references).map(([name, reference]) => {
const resolved = resolve({ name, reference, directory: input.directory, worktree: input.worktree })
if (resolved.kind !== "git") return resolved
const existing = seen.get(resolved.path)
if (!existing) {
seen.set(resolved.path, { name, branch: resolved.branch })
return resolved
}
if (existing.branch === resolved.branch) return resolved
return {
name,
kind: "invalid" as const,
repository: resolved.repository,
message: `Reference conflicts with @${existing.name}: both use ${resolved.path}, but @${existing.name} requests ${branchLabel(existing.branch)} and @${name} requests ${branchLabel(resolved.branch)}`,
}
})
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const cache = yield* RepositoryCache.Service
const scope = yield* Scope.Scope
const flags = yield* RuntimeFlags.Service
const state = yield* InstanceState.make<State>(
Effect.fn("Reference.state")(function* (ctx) {
const cfg = yield* config.get()
const references = resolveAll({
references: ConfigReference.normalize(cfg.reference ?? {}),
directory: ctx.directory,
worktree: ctx.worktree,
})
const materializeByPath = yield* materializers(cache, references)
const materializeAllCached = yield* Effect.cached(materializeAll({ flags, materializers: materializeByPath }))
return { references, materializeAll: materializeAllCached, materializeByPath }
}),
)
return Service.of({
init: Effect.fn("Reference.init")(function* () {
if (!flags.experimentalReferences) return
yield* InstanceState.useEffect(state, (s) => s.materializeAll).pipe(Effect.forkIn(scope), Effect.asVoid)
}),
list: Effect.fn("Reference.list")(function* () {
return yield* InstanceState.use(state, (s) => s.references)
}),
get: Effect.fn("Reference.get")(function* (name: string) {
return yield* InstanceState.use(state, (s) => s.references.find((reference) => reference.name === name))
}),
ensure: Effect.fn("Reference.ensure")(function* (target?: string) {
if (!flags.experimentalReferences) return
const full = normalizedTarget(target)
if (!full) return yield* InstanceState.useEffect(state, (s) => s.materializeAll)
return yield* InstanceState.useEffect(state, (s) => materializeByPath(s.materializeByPath, full))
}),
contains: Effect.fn("Reference.contains")(function* (target?: string) {
if (!flags.experimentalReferences) return false
const full = normalizedTarget(target)
if (!full) return false
return yield* InstanceState.use(state, (s) => containsGitReferencePath(s.references, full))
}),
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export * as Reference from "./reference"
@@ -1,320 +0,0 @@
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Flock } from "@opencode-ai/core/util/flock"
import { Git } from "@/git"
import {
repositoryCachePath,
sameRepositoryReference,
parseRepositoryReference,
parseRemoteRepositoryReference,
validateRepositoryBranch,
InvalidRepositoryBranchError,
InvalidRepositoryReferenceError,
UnsupportedLocalRepositoryError,
type RemoteReference,
} from "@/util/repository"
export type Result = {
repository: string
host: string
remote: string
localPath: string
status: "cached" | "cloned" | "refreshed"
head?: string
branch?: string
}
export type EnsureInput = {
reference: RemoteReference
refresh?: boolean
branch?: string
}
export class InvalidRepositoryError extends Schema.TaggedErrorClass<InvalidRepositoryError>()(
"RepositoryCacheInvalidRepositoryError",
{
repository: Schema.String,
message: Schema.String,
},
) {}
export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchError>()(
"RepositoryCacheInvalidBranchError",
{
branch: Schema.String,
message: Schema.String,
},
) {}
export class CloneFailedError extends Schema.TaggedErrorClass<CloneFailedError>()("RepositoryCacheCloneFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class FetchFailedError extends Schema.TaggedErrorClass<FetchFailedError>()("RepositoryCacheFetchFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class CheckoutFailedError extends Schema.TaggedErrorClass<CheckoutFailedError>()(
"RepositoryCacheCheckoutFailedError",
{
repository: Schema.String,
branch: Schema.String,
message: Schema.String,
},
) {}
export class ResetFailedError extends Schema.TaggedErrorClass<ResetFailedError>()("RepositoryCacheResetFailedError", {
repository: Schema.String,
message: Schema.String,
}) {}
export class LockFailedError extends Schema.TaggedErrorClass<LockFailedError>()("RepositoryCacheLockFailedError", {
localPath: Schema.String,
message: Schema.String,
}) {}
export class CacheOperationError extends Schema.TaggedErrorClass<CacheOperationError>()(
"RepositoryCacheOperationError",
{
operation: Schema.String,
path: Schema.String,
message: Schema.String,
},
) {}
export type Error =
| InvalidRepositoryError
| InvalidBranchError
| CloneFailedError
| FetchFailedError
| CheckoutFailedError
| ResetFailedError
| LockFailedError
| CacheOperationError
export interface Interface {
ensure: (input: EnsureInput) => Effect.Effect<Result, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/RepositoryCache") {}
function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
if (!input.reuse) return "cloned" as const
if (input.branchMatches === false) return "refreshed" as const
if (input.refresh) return "refreshed" as const
return "cached" as const
}
function resetTarget(input: {
requestedBranch?: string
remoteHead: { code: number; stdout: string }
branch: { code: number; stdout: string }
}) {
if (input.requestedBranch) return `origin/${input.requestedBranch}`
if (input.remoteHead.code === 0 && input.remoteHead.stdout) {
return input.remoteHead.stdout.replace(/^refs\/remotes\//, "")
}
if (input.branch.code === 0 && input.branch.stdout) {
return `origin/${input.branch.stdout}`
}
return "HEAD"
}
function errorMessage(error: unknown) {
return error instanceof globalThis.Error ? error.message : String(error)
}
export function isError(error: unknown): error is Error {
return (
error instanceof InvalidRepositoryError ||
error instanceof InvalidBranchError ||
error instanceof CloneFailedError ||
error instanceof FetchFailedError ||
error instanceof CheckoutFailedError ||
error instanceof ResetFailedError ||
error instanceof LockFailedError ||
error instanceof CacheOperationError
)
}
export const parseRemoteReference = Effect.fn("RepositoryCache.parseRemoteReference")(function* (repository: string) {
try {
return parseRemoteRepositoryReference(repository)
} catch (error) {
if (error instanceof InvalidRepositoryReferenceError || error instanceof UnsupportedLocalRepositoryError) {
return yield* new InvalidRepositoryError({ repository: error.repository, message: error.message })
}
return yield* new InvalidRepositoryError({
repository,
message: errorMessage(error),
})
}
})
export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
try {
validateRepositoryBranch(branch)
} catch (error) {
if (error instanceof InvalidRepositoryBranchError) {
return yield* new InvalidBranchError({ branch: error.branch, message: error.message })
}
return yield* new InvalidBranchError({ branch, message: errorMessage(error) })
}
})
const ensureWithServices = Effect.fn("RepositoryCache.ensureWithServices")(function* (
input: EnsureInput,
services: {
fs: FSUtil.Interface
git: Git.Interface
},
) {
if (input.branch) yield* validateBranch(input.branch)
const repository = input.reference.label
const remote = input.reference.remote
const localPath = repositoryCachePath(input.reference)
const cloneTarget = parseRepositoryReference(remote) ?? input.reference
return yield* Effect.acquireUseRelease(
Effect.promise((signal) => Flock.acquire(`repo-clone:${localPath}`, { signal })).pipe(
Effect.catch((error: unknown) =>
Effect.fail(new LockFailedError({ localPath, message: errorMessage(error) || `Failed to lock ${localPath}` })),
),
),
() =>
Effect.gen(function* () {
yield* services.fs.ensureDir(path.dirname(localPath)).pipe(
Effect.catch((error: unknown) =>
Effect.fail(
new CacheOperationError({
operation: "ensure cache directory",
path: localPath,
message: errorMessage(error),
}),
),
),
)
const exists = yield* services.fs.existsSafe(localPath)
const hasGitDir = yield* services.fs.existsSafe(path.join(localPath, ".git"))
const origin = hasGitDir
? yield* services.git.run(["config", "--get", "remote.origin.url"], { cwd: localPath })
: undefined
const originReference = origin?.exitCode === 0 ? parseRepositoryReference(origin.text().trim()) : undefined
const reuse = hasGitDir && Boolean(originReference && sameRepositoryReference(originReference, cloneTarget))
if (exists && !reuse) {
yield* services.fs.remove(localPath, { recursive: true }).pipe(
Effect.catch((error: unknown) =>
Effect.fail(
new CacheOperationError({
operation: "remove stale cache",
path: localPath,
message: errorMessage(error),
}),
),
),
)
}
const currentBranch = hasGitDir ? yield* services.git.branch(localPath) : undefined
const status = statusForRepository({
reuse,
refresh: input.refresh,
branchMatches: input.branch ? currentBranch === input.branch : undefined,
})
if (status === "cloned") {
const clone = yield* services.git.run(
["clone", "--depth", "100", ...(input.branch ? ["--branch", input.branch] : []), "--", remote, localPath],
{ cwd: path.dirname(localPath) },
)
if (clone.exitCode !== 0) {
return yield* new CloneFailedError({
repository,
message: clone.stderr.toString().trim() || clone.text().trim() || `Failed to clone ${repository}`,
})
}
}
if (status === "refreshed") {
const fetch = yield* services.git.run(["fetch", "--all", "--prune"], { cwd: localPath })
if (fetch.exitCode !== 0) {
return yield* new FetchFailedError({
repository,
message: fetch.stderr.toString().trim() || fetch.text().trim() || `Failed to refresh ${repository}`,
})
}
if (input.branch) {
const checkout = yield* services.git.run(["checkout", "-B", input.branch, `origin/${input.branch}`], {
cwd: localPath,
})
if (checkout.exitCode !== 0) {
return yield* new CheckoutFailedError({
repository,
branch: input.branch,
message:
checkout.stderr.toString().trim() || checkout.text().trim() || `Failed to checkout ${input.branch}`,
})
}
}
const remoteHead = yield* services.git.run(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: localPath })
const branch = yield* services.git.run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: localPath })
const target = resetTarget({
requestedBranch: input.branch,
remoteHead: { code: remoteHead.exitCode, stdout: remoteHead.text().trim() },
branch: { code: branch.exitCode, stdout: branch.text().trim() },
})
const reset = yield* services.git.run(["reset", "--hard", target], { cwd: localPath })
if (reset.exitCode !== 0) {
return yield* new ResetFailedError({
repository,
message: reset.stderr.toString().trim() || reset.text().trim() || `Failed to reset ${repository}`,
})
}
}
const head = yield* services.git.run(["rev-parse", "HEAD"], { cwd: localPath })
const branch = yield* services.git.branch(localPath)
const headText = head.exitCode === 0 ? head.text().trim() : undefined
return {
repository,
host: input.reference.host,
remote,
localPath,
status,
head: headText,
branch,
} satisfies Result
}),
(lock) => Effect.promise(() => lock.release()).pipe(Effect.ignore),
)
})
export const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const git = yield* Git.Service
return Service.of({
ensure: Effect.fn("RepositoryCache.ensure")(function* (input) {
return yield* ensureWithServices(input, { fs, git })
}),
})
}),
)
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
)
export * as RepositoryCache from "./repository-cache"
@@ -17,7 +17,6 @@ import { ProjectCopyApi } from "./groups/project-copy"
import { ProviderApi } from "./groups/provider"
import { PtyApi, PtyConnectApi } from "./groups/pty"
import { QuestionApi } from "./groups/question"
import { ReferenceApi } from "./groups/reference"
import { SessionApi } from "./groups/session"
import { SyncApi } from "./groups/sync"
import { TuiApi } from "./groups/tui"
@@ -61,7 +60,6 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
.addHttpApi(QuestionApi)
.addHttpApi(PermissionApi)
.addHttpApi(ProviderApi)
.addHttpApi(ReferenceApi)
.addHttpApi(SessionApi)
.addHttpApi(SyncApi)
.addHttpApi(TuiApi)
@@ -1,60 +0,0 @@
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
export const ReferenceDescriptor = Schema.Union([
Schema.Struct({
name: Schema.String,
kind: Schema.Literal("local"),
path: Schema.String,
}),
Schema.Struct({
name: Schema.String,
kind: Schema.Literal("git"),
repository: Schema.String,
path: Schema.String,
branch: Schema.optional(Schema.String),
}),
Schema.Struct({
name: Schema.String,
kind: Schema.Literal("invalid"),
repository: Schema.optional(Schema.String),
message: Schema.String,
}),
]).annotate({ identifier: "ReferenceDescriptor" })
export const ReferenceApi = HttpApi.make("reference")
.add(
HttpApiGroup.make("reference")
.add(
HttpApiEndpoint.get("list", "/reference", {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(ReferenceDescriptor), "Resolved configured references"),
}).annotateMerge(
OpenApi.annotations({
identifier: "reference.list",
summary: "List configured references",
description: "List configured references resolved in the current workspace.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "reference",
description: "Configured reference routes.",
}),
)
.middleware(InstanceContextMiddleware)
.middleware(WorkspaceRoutingMiddleware)
.middleware(Authorization),
)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)
@@ -1,27 +0,0 @@
import { Reference } from "@/reference/reference"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
export const referenceHandlers = HttpApiBuilder.group(InstanceHttpApi, "reference", (handlers) =>
Effect.gen(function* () {
const reference = yield* Reference.Service
return handlers.handle("list", () =>
reference.list().pipe(
Effect.map((references) =>
references.map((item) => {
if (item.kind !== "git") return item
return {
name: item.name,
kind: item.kind,
repository: item.repository,
path: item.path,
...(item.branch !== undefined ? { branch: item.branch } : {}),
}
}),
),
),
)
}),
)
@@ -35,7 +35,6 @@ import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Provider } from "@/provider/provider"
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { Question } from "@/question"
import { Reference } from "@/reference/reference"
import { Session } from "@/session/session"
import { SessionCompaction } from "@/session/compaction"
import { LLM } from "@/session/llm"
@@ -86,7 +85,6 @@ import { projectCopyHandlers } from "./handlers/project-copy"
import { providerHandlers } from "./handlers/provider"
import { ptyConnectHandlers, ptyHandlers } from "./handlers/pty"
import { questionHandlers } from "./handlers/question"
import { referenceHandlers } from "./handlers/reference"
import { sessionHandlers } from "./handlers/session"
import { syncHandlers } from "./handlers/sync"
import { tuiHandlers } from "./handlers/tui"
@@ -149,7 +147,6 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
projectCopyHandlers,
ptyHandlers,
questionHandlers,
referenceHandlers,
permissionHandlers,
providerHandlers,
sessionHandlers,
@@ -237,7 +234,6 @@ export function createRoutes(
Provider.defaultLayer,
PtyTicket.defaultLayer,
Question.defaultLayer,
Reference.defaultLayer,
Ripgrep.defaultLayer,
RuntimeFlags.defaultLayer,
Session.defaultLayer,
+2 -83
View File
@@ -52,12 +52,10 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AgentAttachment, FileAttachment, Prompt, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
import { Reference } from "@/reference/reference"
import { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/core/session/prompt"
import * as DateTime from "effect/DateTime"
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { referencePromptMetadata, referenceTextPart } from "./prompt/reference"
import { SessionReminders } from "./reminders"
import { SessionTools } from "./tools"
import { LLMEvent } from "@opencode-ai/llm"
@@ -122,7 +120,6 @@ export const layer = Layer.effect(
const summary = yield* SessionSummary.Service
const sys = yield* SystemPrompt.Service
const llm = yield* LLM.Service
const references = yield* Reference.Service
const events = yield* EventV2Bridge.Service
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service
@@ -140,46 +137,10 @@ export const layer = Layer.effect(
yield* state.cancel(sessionID)
})
const resolveReferenceParts = Effect.fnUntraced(function* (template: string) {
const parts: Types.DeepMutable<PromptInput["parts"]> = []
const seen = new Set<string>()
yield* Effect.forEach(
ConfigMarkdown.files(template),
Effect.fnUntraced(function* (match) {
const name = match[1]
if (!name) return
const alias = name.split("/")[0]
if (!alias || seen.has(alias)) return
const reference = yield* references.get(alias)
if (!reference) return
seen.add(alias)
const start = match.index ?? 0
const source = { value: match[0], start, end: start + match[0].length }
if (reference.kind === "invalid") {
parts.push(referenceTextPart({ reference, source }))
return
}
yield* references.ensure(reference.path)
parts.push({
type: "file",
url: pathToFileURL(reference.path).href,
filename: alias,
mime: "application/x-directory",
source: { type: "file", text: source, path: alias },
})
}),
{ concurrency: 1, discard: true },
)
return parts
})
const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
const ctx = yield* InstanceState.context
const parts: Types.DeepMutable<PromptInput["parts"]> = [
{ type: "text", text: template },
...(yield* resolveReferenceParts(template)),
]
const files = ConfigMarkdown.files(template)
const seen = new Set<string>()
@@ -191,10 +152,6 @@ export const layer = Layer.effect(
if (seen.has(name)) return
seen.add(name)
const slash = name.indexOf("/")
const alias = slash === -1 ? name : name.slice(0, slash)
if (yield* references.get(alias)) return
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(ctx.worktree, name)
@@ -1019,22 +976,7 @@ export const layer = Layer.effect(
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
})
const submittedParts: Types.DeepMutable<PromptInput["parts"]> = [...input.parts]
const attachedReferences = new Set(
input.parts.flatMap((part) =>
part.type === "file" && part.mime === "application/x-directory" ? [part.url] : [],
),
)
for (const part of input.parts) {
if (part.type !== "text" || part.synthetic) continue
for (const reference of yield* resolveReferenceParts(part.text)) {
if (reference.type === "file" && attachedReferences.has(reference.url)) continue
if (reference.type === "file") attachedReferences.add(reference.url)
submittedParts.push(reference)
}
}
const resolvedParts = yield* Effect.forEach(submittedParts, resolvePart, { concurrency: "unbounded" }).pipe(
const resolvedParts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
Effect.map((x) => x.flat().map(assign)),
)
@@ -1092,26 +1034,6 @@ export const layer = Layer.effect(
if (part.type === "text") {
if (part.synthetic) result.synthetic.push(part.text)
else result.text.push(part.text)
const reference = referencePromptMetadata(part.metadata?.reference)
if (reference) {
result.references.push(
new ReferenceAttachment({
name: reference.name,
kind: reference.kind,
uri: reference.path ? pathToFileURL(reference.path).href : undefined,
repository: reference.repository,
branch: reference.branch,
target: reference.target,
targetUri: reference.targetPath ? pathToFileURL(reference.targetPath).href : undefined,
problem: reference.problem,
source: new Source({
start: reference.source.start,
end: reference.source.end,
text: reference.source.value,
}),
}),
)
}
}
if (part.type === "file") {
result.files.push(
@@ -1149,7 +1071,6 @@ export const layer = Layer.effect(
text: [] as string[],
files: [] as FileAttachment[],
agents: [] as AgentAttachment[],
references: [] as ReferenceAttachment[],
synthetic: [] as string[],
},
)
@@ -1164,7 +1085,6 @@ export const layer = Layer.effect(
text: nextPrompt.text.join("\n"),
files: nextPrompt.files,
agents: nextPrompt.agents,
references: nextPrompt.references,
}),
})
}
@@ -1642,7 +1562,6 @@ export const defaultLayer = Layer.suspend(() =>
Database.defaultLayer,
SystemPrompt.defaultLayer,
LLM.defaultLayer,
Reference.defaultLayer,
CrossSpawnSpawner.defaultLayer,
RuntimeFlags.defaultLayer,
EventV2Bridge.defaultLayer,
@@ -1,72 +0,0 @@
import { Option, Schema } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageV2 } from "../message-v2"
import { Reference } from "@/reference/reference"
const Source = Schema.Struct({
value: Schema.String,
start: Schema.Number,
end: Schema.Number,
})
export const ReferencePromptMetadata = Schema.Struct({
name: Schema.String,
kind: Schema.Literals(["local", "git", "invalid"]),
path: Schema.optional(Schema.String),
repository: Schema.optional(Schema.String),
branch: Schema.optional(Schema.String),
target: Schema.optional(Schema.String),
targetPath: Schema.optional(Schema.String),
problem: Schema.optional(Schema.String),
source: Source,
})
export type ReferencePromptMetadata = typeof ReferencePromptMetadata.Type
const decodeReferencePromptMetadata = Schema.decodeUnknownOption(ReferencePromptMetadata)
export function referencePromptMetadata(input: unknown) {
return Option.getOrUndefined(decodeReferencePromptMetadata(input))
}
export function referenceTextPart(input: {
reference: Reference.Resolved
source: ReferencePromptMetadata["source"]
target?: string
targetPath?: string
problem?: string
}): SessionV1.TextPartInput {
const metadata: ReferencePromptMetadata = {
name: input.reference.name,
kind: input.reference.kind,
...(input.reference.kind === "invalid"
? { repository: input.reference.repository }
: { path: input.reference.path }),
...(input.reference.kind === "git"
? { repository: input.reference.repository, branch: input.reference.branch }
: {}),
...(input.target === undefined ? {} : { target: input.target }),
...(input.targetPath ? { targetPath: input.targetPath } : {}),
problem: input.problem ?? (input.reference.kind === "invalid" ? input.reference.message : undefined),
source: input.source,
}
const label = metadata.target === undefined ? `@${metadata.name}` : `@${metadata.name}/${metadata.target}`
return {
type: "text",
synthetic: true,
text: [
`Referenced configured reference ${label}.`,
...(metadata.kind === "local" ? ["Kind: local directory"] : []),
...(metadata.kind === "git" ? ["Kind: git repository"] : []),
...(metadata.repository ? [`Repository: ${metadata.repository}`] : []),
...(metadata.branch ? [`Branch/ref: ${metadata.branch}`] : []),
...(metadata.path ? [`Reference root: ${metadata.path}`] : []),
...(metadata.targetPath ? [`Resolved path: ${metadata.targetPath}`] : []),
...(metadata.problem
? [`Problem: ${metadata.problem}`]
: ["Inspect the configured reference with Read, Glob, and Grep when useful."]),
].join("\n"),
metadata: { reference: metadata },
}
}
export * as ReferencePrompt from "./reference"
+1 -4
View File
@@ -6,7 +6,6 @@ import { Search } from "@opencode-ai/core/filesystem/search"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
@@ -19,7 +18,6 @@ export const GlobTool = Tool.define(
"glob",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const reference = yield* Reference.Service
const searchSvc = yield* Search.Service
return {
@@ -40,13 +38,12 @@ export const GlobTool = Tool.define(
let search = params.path ?? ins.directory
search = path.isAbsolute(search) ? search : path.resolve(ins.directory, search)
yield* reference.ensure(search)
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (info?.type === "File") {
throw new Error(`glob path must be a directory: ${search}`)
}
yield* assertExternalDirectoryEffect(ctx, search, {
bypass: yield* reference.contains(search),
bypass: false,
kind: "directory",
})
+1 -4
View File
@@ -6,7 +6,6 @@ import { Search } from "@opencode-ai/core/filesystem/search"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
const MAX_LINE_LENGTH = 2000
@@ -25,7 +24,6 @@ export const GrepTool = Tool.define(
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const searchSvc = yield* Search.Service
const reference = yield* Reference.Service
return {
description: DESCRIPTION,
@@ -56,10 +54,9 @@ export const GrepTool = Tool.define(
const requested = path.isAbsolute(params.path ?? ins.directory)
? (params.path ?? ins.directory)
: path.join(ins.directory, params.path ?? ".")
yield* reference.ensure(requested)
const requestedInfo = yield* fs.stat(requested).pipe(Effect.catch(() => Effect.succeed(undefined)))
yield* assertExternalDirectoryEffect(ctx, requested, {
bypass: yield* reference.contains(requested),
bypass: false,
kind: requestedInfo?.type === "Directory" ? "directory" : "file",
})
+2 -5
View File
@@ -10,7 +10,6 @@ import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { Search } from "@opencode-ai/core/filesystem/search"
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
import { Reference } from "@/reference/reference"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -66,14 +65,13 @@ type Metadata = {
export const ReadTool = Tool.define<
typeof Parameters,
Metadata,
FSUtil.Service | Instruction.Service | LSP.Service | Reference.Service | Search.Service | Scope.Scope
FSUtil.Service | Instruction.Service | LSP.Service | Search.Service | Scope.Scope
>(
"read",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const instruction = yield* Instruction.Service
const lsp = yield* LSP.Service
const reference = yield* Reference.Service
const search = yield* Search.Service
const scope = yield* Scope.Scope
@@ -243,7 +241,6 @@ export const ReadTool = Tool.define<
if (process.platform === "win32") {
filepath = FSUtil.normalizePath(filepath)
}
yield* reference.ensure(filepath)
const title = path.relative(instance.worktree, filepath)
const stat = yield* fs.stat(filepath).pipe(
@@ -254,7 +251,7 @@ export const ReadTool = Tool.define<
)
yield* assertExternalDirectoryEffect(ctx, filepath, {
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]) || (yield* reference.contains(filepath)),
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
kind: stat?.type === "Directory" ? "directory" : "file",
})
-3
View File
@@ -46,7 +46,6 @@ import { EventV2Bridge } from "@/event-v2-bridge"
import { Agent } from "../agent/agent"
import { Skill } from "../skill"
import { Permission } from "@/permission"
import { Reference } from "@/reference/reference"
import { BackgroundJob } from "@/background/job"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
@@ -91,7 +90,6 @@ export const layer: Layer.Layer<
| Session.Service
| BackgroundJob.Service
| Provider.Service
| Reference.Service
| LSP.Service
| Instruction.Service
| FSUtil.Service
@@ -350,7 +348,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Session.defaultLayer),
Layer.provide(BackgroundJob.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Reference.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(FSUtil.defaultLayer),