feat(core): add project reference guidance (#31601)
This commit is contained in:
parent
0fc33e2a06
commit
8a2cfc00c9
@ -2,8 +2,15 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {},
|
||||
"permission": {},
|
||||
"reference": {
|
||||
"effect": "github.com/Effect-TS/effect-smol",
|
||||
"references": {
|
||||
"effect": {
|
||||
"repository": "github.com/Effect-TS/effect-smol",
|
||||
"description": "Use for Effect v4 and effect-smol implementation details",
|
||||
},
|
||||
"opencode-local": {
|
||||
"path": "~/.local/share/opencode",
|
||||
"description": "Contains opencode logs and data",
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
|
||||
@ -33,11 +33,15 @@ export const Plugin = {
|
||||
path: AbsolutePath.make(
|
||||
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||
),
|
||||
description: typeof entry === "string" ? undefined : entry.description,
|
||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||
})
|
||||
: new Reference.GitSource({
|
||||
type: "git",
|
||||
repository: typeof entry === "string" ? entry : entry.repository,
|
||||
branch: typeof entry === "string" ? undefined : entry.branch,
|
||||
description: typeof entry === "string" ? undefined : entry.description,
|
||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,10 +5,14 @@ import { Schema } from "effect"
|
||||
export class Git extends Schema.Class<Git>("ConfigV2.Reference.Git")({
|
||||
repository: Schema.String,
|
||||
branch: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Local extends Schema.Class<Local>("ConfigV2.Reference.Local")({
|
||||
path: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Entry = Schema.Union([Schema.String, Git, Local])
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Layer, LayerMap } from "effect"
|
||||
import { Effect, Layer, LayerMap } from "effect"
|
||||
import { Location } from "./location"
|
||||
import { Policy } from "./policy"
|
||||
import { Config } from "./config"
|
||||
@ -23,6 +23,7 @@ import { Watcher } from "./filesystem/watcher"
|
||||
import { LocationMutation } from "./location-mutation"
|
||||
import { FileMutation } from "./file-mutation"
|
||||
import { Reference } from "./reference"
|
||||
import { ReferenceGuidance } from "./reference/guidance"
|
||||
import { RepositoryCache } from "./repository-cache"
|
||||
import { Pty } from "./pty"
|
||||
import { SkillV2 } from "./skill"
|
||||
@ -45,6 +46,9 @@ import { FetchHttpClient } from "effect/unstable/http"
|
||||
|
||||
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
|
||||
lookup: (ref: Location.Ref) => {
|
||||
const boot = Layer.effectDiscard(
|
||||
Effect.logInfo("booting location services", { directory: ref.directory, workspaceID: ref.workspaceID }),
|
||||
)
|
||||
const location = Location.layer(ref)
|
||||
const systemContext = SystemContextBuiltIns.locationLayer
|
||||
const base = Layer.mergeAll(
|
||||
@ -74,6 +78,7 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
|
||||
const image = Image.layer.pipe(Layer.provide(services))
|
||||
const mutation = FileMutation.locationLayer.pipe(Layer.provide(services))
|
||||
const skillGuidance = SkillGuidance.locationLayer.pipe(Layer.provide(services))
|
||||
const referenceGuidance = ReferenceGuidance.locationLayer.pipe(Layer.provide(services))
|
||||
const todos = SessionTodo.layer.pipe(Layer.provide(services))
|
||||
const questions = QuestionV2.locationLayer.pipe(Layer.provide(services))
|
||||
const builtInTools = BuiltInTools.locationLayer.pipe(
|
||||
@ -89,10 +94,21 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
|
||||
Layer.provide(services),
|
||||
Layer.provide(model),
|
||||
Layer.provide(skillGuidance),
|
||||
Layer.provide(referenceGuidance),
|
||||
)
|
||||
return Layer.mergeAll(services, image, mutation, resources, todos, questions, model, runner, builtInTools).pipe(
|
||||
Layer.fresh,
|
||||
)
|
||||
return Layer.mergeAll(
|
||||
boot,
|
||||
services,
|
||||
image,
|
||||
mutation,
|
||||
resources,
|
||||
todos,
|
||||
questions,
|
||||
model,
|
||||
runner,
|
||||
builtInTools,
|
||||
referenceGuidance,
|
||||
).pipe(Layer.fresh)
|
||||
},
|
||||
idleTimeToLive: "60 minutes",
|
||||
dependencies: [
|
||||
|
||||
@ -5,11 +5,10 @@ import { WorkspaceV2 } from "./workspace"
|
||||
|
||||
export * as Location from "./location"
|
||||
|
||||
export const Ref = Schema.Struct({
|
||||
export class Ref extends Schema.Class<Ref>("Location.Ref")({
|
||||
directory: AbsolutePath,
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID),
|
||||
}).annotate({ identifier: "Location.Ref" })
|
||||
export type Ref = typeof Ref.Type
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID).pipe(Schema.withConstructorDefault(Effect.succeed(undefined))),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("Location.Info")({
|
||||
directory: AbsolutePath,
|
||||
|
||||
@ -73,6 +73,19 @@ Every field is optional.
|
||||
"urls": ["https://example.com/.well-known/skills/"]
|
||||
},
|
||||
|
||||
"references": {
|
||||
"docs": {
|
||||
"path": "../docs",
|
||||
"description": "Use for product behavior and documentation conventions"
|
||||
},
|
||||
"sdk": {
|
||||
"repository": "owner/sdk",
|
||||
"branch": "main",
|
||||
"description": "Use for SDK implementation details",
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
|
||||
"agent": {
|
||||
"my-agent": {
|
||||
"model": "anthropic/claude-sonnet-4-6",
|
||||
@ -136,6 +149,7 @@ Shape notes worth being explicit about:
|
||||
|
||||
- `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`.
|
||||
- `skills` is an object with `paths` and/or `urls`, not an array.
|
||||
- `references` is an object keyed by alias. Each value is a local path, Git repository, or string shorthand.
|
||||
- `agent` is an object keyed by agent name, not an array.
|
||||
- `plugin` is an array of strings or `[name, options]` tuples, not an object.
|
||||
- `mcp[name].command` is an array of strings, never a single string. `type` is required.
|
||||
@ -172,6 +186,38 @@ Register skills from non-default locations via `skills.paths` (scanned
|
||||
recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of
|
||||
skills).
|
||||
|
||||
## References
|
||||
|
||||
References make local directories and Git repositories outside the active
|
||||
project available as supporting context. Configure them under `references`,
|
||||
keyed by the alias used in `@` autocomplete:
|
||||
|
||||
```json
|
||||
{
|
||||
"references": {
|
||||
"docs": {
|
||||
"path": "../product-docs",
|
||||
"description": "Use for product behavior and terminology"
|
||||
},
|
||||
"effect": {
|
||||
"repository": "Effect-TS/effect",
|
||||
"branch": "main",
|
||||
"description": "Use for Effect implementation details"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Local `path` values may be relative to the declaring config, absolute, or use
|
||||
`~/`. Git `repository` values accept Git URLs, host/path references, and GitHub
|
||||
`owner/repo` shorthand; `branch` is optional. Both forms support optional
|
||||
`description` and `hidden` fields.
|
||||
|
||||
- Only references with a `description` are advertised to agents in system context.
|
||||
- `hidden: true` removes a reference from TUI `@` autocomplete only. It remains available to agents and by direct path.
|
||||
- Reference directories are automatically allowed through the external-directory boundary; normal read/edit/tool permissions still apply.
|
||||
- String shorthand is supported: use `"docs": "../docs"` for local paths or `"effect": "Effect-TS/effect"` for Git repositories.
|
||||
|
||||
## Agents
|
||||
|
||||
Two ways to define an agent. Use the file form for anything non-trivial.
|
||||
|
||||
@ -9,21 +9,19 @@ import { RepositoryCache } from "./repository-cache"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { State } from "./state"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Reference.Info")({
|
||||
name: Schema.String,
|
||||
path: AbsolutePath,
|
||||
source: Schema.suspend(() => Source),
|
||||
}) {}
|
||||
|
||||
export class LocalSource extends Schema.Class<LocalSource>("Reference.LocalSource")({
|
||||
type: Schema.Literal("local"),
|
||||
path: AbsolutePath,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class GitSource extends Schema.Class<GitSource>("Reference.GitSource")({
|
||||
type: Schema.Literal("git"),
|
||||
repository: Schema.String,
|
||||
branch: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type"))
|
||||
@ -33,6 +31,14 @@ export const Event = {
|
||||
Updated: EventV2.define({ type: "reference.updated", schema: {} }),
|
||||
}
|
||||
|
||||
export class Info extends Schema.Class<Info>("Reference.Info")({
|
||||
name: Schema.String,
|
||||
path: AbsolutePath,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
source: Source,
|
||||
}) {}
|
||||
|
||||
type Data = {
|
||||
sources: Map<string, Source>
|
||||
}
|
||||
@ -71,7 +77,16 @@ export const layer = Layer.effect(
|
||||
const seen = new Map<string, string | undefined>()
|
||||
for (const [name, source] of editor.list()) {
|
||||
if (source.type === "local") {
|
||||
materialized.set(name, new Info({ name, path: source.path, source }))
|
||||
materialized.set(
|
||||
name,
|
||||
new Info({
|
||||
name,
|
||||
path: source.path,
|
||||
description: source.description,
|
||||
hidden: source.hidden,
|
||||
source,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
const repository = Repository.parse(source.repository)
|
||||
@ -86,7 +101,16 @@ export const layer = Layer.effect(
|
||||
const target = Repository.cachePath(global.repos, repository)
|
||||
if (seen.has(target) && seen.get(target) !== source.branch) continue
|
||||
seen.set(target, source.branch)
|
||||
materialized.set(name, new Info({ name, path: AbsolutePath.make(target), source }))
|
||||
materialized.set(
|
||||
name,
|
||||
new Info({
|
||||
name,
|
||||
path: AbsolutePath.make(target),
|
||||
description: source.description,
|
||||
hidden: source.hidden,
|
||||
source,
|
||||
}),
|
||||
)
|
||||
yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("failed to materialize reference", {
|
||||
|
||||
69
packages/core/src/reference/guidance.ts
Normal file
69
packages/core/src/reference/guidance.ts
Normal file
@ -0,0 +1,69 @@
|
||||
export * as ReferenceGuidance from "./guidance"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { Reference } from "../reference"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
const Summary = Schema.Struct({
|
||||
name: Schema.String,
|
||||
path: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const render = (references: ReadonlyArray<typeof Summary.Type>) =>
|
||||
[
|
||||
"Project references provide additional directories that can be accessed when relevant.",
|
||||
"<available_references>",
|
||||
...references.flatMap((reference) => [
|
||||
" <reference>",
|
||||
` <name>${reference.name}</name>`,
|
||||
` <path>${reference.path}</path>`,
|
||||
...(reference.description === undefined ? [] : [` <description>${reference.description}</description>`]),
|
||||
" </reference>",
|
||||
]),
|
||||
"</available_references>",
|
||||
].join("\n")
|
||||
|
||||
export interface Interface {
|
||||
readonly load: () => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ReferenceGuidance") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const references = yield* Reference.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("ReferenceGuidance.load")(function* () {
|
||||
yield* boot.wait()
|
||||
const available = (yield* references.list())
|
||||
.filter((reference) => reference.description !== undefined)
|
||||
.map((reference) => ({
|
||||
name: reference.name,
|
||||
path: reference.path,
|
||||
description: reference.description,
|
||||
}))
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
if (available.length === 0) return SystemContext.empty
|
||||
return SystemContext.make({
|
||||
key: SystemContext.Key.make("core/reference-guidance"),
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
load: Effect.succeed(available),
|
||||
baseline: render,
|
||||
update: (_previous, current) =>
|
||||
[
|
||||
"The available project references have changed. This list supersedes the previous reference list.",
|
||||
render(current),
|
||||
].join("\n"),
|
||||
removed: () => "Project reference guidance is no longer available. Do not use previously listed references.",
|
||||
})
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
@ -161,10 +161,10 @@ export const layer = Layer.effect(
|
||||
args: [
|
||||
"--no-config",
|
||||
"--files",
|
||||
"--glob=!**/.git/**",
|
||||
...(input.hidden ? ["--hidden"] : []),
|
||||
...(input.follow ? ["--follow"] : []),
|
||||
`--glob=${input.pattern}`,
|
||||
"--glob=!**/.git/**",
|
||||
".",
|
||||
],
|
||||
parse: (line) =>
|
||||
@ -195,10 +195,10 @@ export const layer = Layer.effect(
|
||||
args: [
|
||||
"--no-config",
|
||||
"--files",
|
||||
"--glob=!**/.git/**",
|
||||
...(input.hidden ? ["--hidden"] : []),
|
||||
...(input.follow ? ["--follow"] : []),
|
||||
`--glob=${input.pattern}`,
|
||||
...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]),
|
||||
"--glob=!**/.git/**",
|
||||
".",
|
||||
],
|
||||
parse: (line) => {
|
||||
@ -226,9 +226,9 @@ export const layer = Layer.effect(
|
||||
"--no-config",
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--glob=!**/.git/**",
|
||||
"--no-messages",
|
||||
...(input.include ? [`--glob=${input.include}`] : []),
|
||||
"--glob=!**/.git/**",
|
||||
"--",
|
||||
input.pattern,
|
||||
input.file ?? ".",
|
||||
|
||||
@ -19,6 +19,7 @@ import { QuestionV2 } from "../../question"
|
||||
import { SystemContext } from "../../system-context/index"
|
||||
import { SystemContextRegistry } from "../../system-context/registry"
|
||||
import { SkillGuidance } from "../../skill/guidance"
|
||||
import { ReferenceGuidance } from "../../reference/guidance"
|
||||
import { ToolRegistry } from "../../tool/registry"
|
||||
import { ToolOutputStore } from "../../tool-output-store"
|
||||
import { SessionContextEpoch } from "../context-epoch"
|
||||
@ -98,6 +99,7 @@ export const layer = Layer.effect(
|
||||
const location = yield* Location.Service
|
||||
const systemContext = yield* SystemContextRegistry.Service
|
||||
const skillGuidance = yield* SkillGuidance.Service
|
||||
const referenceGuidance = yield* ReferenceGuidance.Service
|
||||
const config = yield* Config.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const compaction = SessionCompaction.make({ events, llm, config: yield* config.entries() })
|
||||
@ -166,9 +168,9 @@ export const layer = Layer.effect(
|
||||
|
||||
const sameModel = Schema.toEquivalence(Schema.UndefinedOr(ModelV2.Ref))
|
||||
const loadSystemContext = (agent: AgentV2.Selection) =>
|
||||
Effect.all([systemContext.load(), skillGuidance.load(agent)], { concurrency: "unbounded" }).pipe(
|
||||
Effect.map(SystemContext.combine),
|
||||
)
|
||||
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
|
||||
concurrency: "unbounded",
|
||||
}).pipe(Effect.map(SystemContext.combine))
|
||||
|
||||
const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
|
||||
@ -3,6 +3,7 @@ export * as ConfigV1 from "./config"
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt, PositiveInt, type DeepMutable } from "../../schema"
|
||||
import { ConfigExperimental } from "../../config/experimental"
|
||||
import { ConfigReference } from "../../config/reference"
|
||||
import { ConfigAgentV1 } from "./agent"
|
||||
import { ConfigAttachmentV1 } from "./attachment"
|
||||
import { ConfigCommandV1 } from "./command"
|
||||
@ -13,7 +14,6 @@ import { ConfigMCPV1 } from "./mcp"
|
||||
import { ConfigPermissionV1 } from "./permission"
|
||||
import { ConfigPluginV1 } from "./plugin"
|
||||
import { ConfigProviderV1 } from "./provider"
|
||||
import { ConfigReferenceV1 } from "./reference"
|
||||
import { ConfigServerV1 } from "./server"
|
||||
import { ConfigSkillsV1 } from "./skills"
|
||||
|
||||
@ -42,7 +42,7 @@ export const Info = Schema.Struct({
|
||||
description: "Command configuration, see https://opencode.ai/docs/commands",
|
||||
}),
|
||||
skills: Schema.optional(ConfigSkillsV1.Info).annotate({ description: "Additional skill folder paths" }),
|
||||
reference: Schema.optional(ConfigReferenceV1.Info).annotate({
|
||||
references: Schema.optional(ConfigReference.Info).annotate({
|
||||
description: "Named git or local directory references",
|
||||
}),
|
||||
watcher: Schema.optional(Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))) })),
|
||||
|
||||
@ -12,7 +12,6 @@ const keys = new Set([
|
||||
"logLevel",
|
||||
"server",
|
||||
"command",
|
||||
"reference",
|
||||
"snapshot",
|
||||
"plugin",
|
||||
"autoshare",
|
||||
@ -63,7 +62,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
|
||||
skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
|
||||
commands: info.command,
|
||||
instructions: info.instructions,
|
||||
references: info.reference,
|
||||
references: info.references,
|
||||
plugins: info.plugin?.map((plugin) =>
|
||||
typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
|
||||
),
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
export * as ConfigReferenceV1 from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
const Git = Schema.Struct({
|
||||
repository: Schema.String.annotate({
|
||||
description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand",
|
||||
}),
|
||||
branch: Schema.optional(Schema.String).annotate({
|
||||
description: "Branch or ref to clone and inspect",
|
||||
}),
|
||||
})
|
||||
|
||||
const Local = Schema.Struct({
|
||||
path: Schema.String.annotate({
|
||||
description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory",
|
||||
}),
|
||||
})
|
||||
|
||||
export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
|
||||
export type Entry = Schema.Schema.Type<typeof Entry>
|
||||
|
||||
export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
@ -464,7 +464,9 @@ describe("Config", () => {
|
||||
["@my-org/audit-plugin", { endpoint: "https://audit.example.com" }],
|
||||
],
|
||||
skills: { paths: ["./skills"], urls: ["https://example.com/.well-known/skills/"] },
|
||||
reference: { docs: { path: "../docs" } },
|
||||
references: {
|
||||
docs: { path: "../docs", description: "Use for product documentation", hidden: true },
|
||||
},
|
||||
attachment: { image: { auto_resize: false, max_width: 1200 } },
|
||||
provider: {
|
||||
custom: {
|
||||
@ -540,7 +542,9 @@ describe("Config", () => {
|
||||
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
|
||||
])
|
||||
expect(documents[0]?.info.skills).toEqual(["./skills", "https://example.com/.well-known/skills/"])
|
||||
expect(documents[0]?.info.references).toEqual({ docs: { path: "../docs" } })
|
||||
expect(documents[0]?.info.references).toEqual({
|
||||
docs: { path: "../docs", description: "Use for product documentation", hidden: true },
|
||||
})
|
||||
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
|
||||
expect(documents[0]?.info.providers?.custom).toMatchObject({
|
||||
request: { body: { apiKey: "secret" } },
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Equal, Hash, Layer, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/core/public"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
@ -44,6 +45,16 @@ const it = testEffect(
|
||||
)
|
||||
|
||||
describe("LocationServiceMap", () => {
|
||||
it.effect("compares equivalent location refs by value", () =>
|
||||
Effect.sync(() => {
|
||||
const directory = AbsolutePath.make("/project")
|
||||
expect(Equal.equals(Location.Ref.make({ directory }), Location.Ref.make({ directory }))).toBe(true)
|
||||
expect(Hash.hash(Location.Ref.make({ directory }))).toBe(
|
||||
Hash.hash(Location.Ref.make({ directory, workspaceID: undefined })),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("isolates location state while sharing location policy with catalog", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
|
||||
@ -79,7 +90,10 @@ describe("LocationServiceMap", () => {
|
||||
providers: yield* catalog.provider.all(),
|
||||
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
|
||||
}
|
||||
}).pipe(Effect.scoped, Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(directory) })))
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
|
||||
const blockedState = yield* update(blocked.path)
|
||||
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)
|
||||
|
||||
77
packages/core/test/reference-guidance.test.ts
Normal file
77
packages/core/test/reference-guidance.test.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context/index"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
describe("ReferenceGuidance", () => {
|
||||
it.effect("lists available references in the system context", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
|
||||
expect(generation.baseline).toContain("<available_references>")
|
||||
expect(generation.baseline).toContain("<name>docs</name>")
|
||||
expect(generation.baseline).toContain("<path>/docs</path>")
|
||||
expect(generation.baseline).toContain("<description>Use for product documentation</description>")
|
||||
}).pipe(
|
||||
Effect.provide(ReferenceGuidance.layer),
|
||||
Effect.provide(
|
||||
Layer.mock(Reference.Service, {
|
||||
list: () =>
|
||||
Effect.succeed([
|
||||
new Reference.Info({
|
||||
name: "docs",
|
||||
path: AbsolutePath.make("/docs"),
|
||||
description: "Use for product documentation",
|
||||
source: new Reference.LocalSource({
|
||||
type: "local",
|
||||
path: AbsolutePath.make("/docs"),
|
||||
description: "Use for product documentation",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("omits guidance when no references are available", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
expect(generation.baseline).toBe("")
|
||||
}).pipe(
|
||||
Effect.provide(ReferenceGuidance.layer),
|
||||
Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })),
|
||||
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("omits references without descriptions", () =>
|
||||
Effect.gen(function* () {
|
||||
const guidance = yield* ReferenceGuidance.Service
|
||||
const generation = yield* SystemContext.initialize(yield* guidance.load())
|
||||
expect(generation.baseline).toBe("")
|
||||
}).pipe(
|
||||
Effect.provide(ReferenceGuidance.layer),
|
||||
Effect.provide(
|
||||
Layer.mock(Reference.Service, {
|
||||
list: () =>
|
||||
Effect.succeed([
|
||||
new Reference.Info({
|
||||
name: "docs",
|
||||
path: AbsolutePath.make("/docs"),
|
||||
source: new Reference.LocalSource({ type: "local", path: AbsolutePath.make("/docs") }),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
|
||||
),
|
||||
)
|
||||
})
|
||||
@ -19,10 +19,16 @@ describe("Reference", () => {
|
||||
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 })))
|
||||
const source = new Reference.LocalSource({
|
||||
type: "local",
|
||||
path,
|
||||
description: "Use for API documentation",
|
||||
hidden: true,
|
||||
})
|
||||
yield* update((editor) => editor.add("docs", source))
|
||||
|
||||
expect(yield* references.list()).toEqual([
|
||||
new Reference.Info({ name: "docs", path, source: new Reference.LocalSource({ type: "local", path }) }),
|
||||
new Reference.Info({ name: "docs", path, description: "Use for API documentation", hidden: true, source }),
|
||||
])
|
||||
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
@ -58,4 +64,33 @@ describe("Reference", () => {
|
||||
Effect.provide(Global.defaultLayer),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("preserves configured Git descriptions", () =>
|
||||
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",
|
||||
description: "Use for SDK implementation details",
|
||||
})
|
||||
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)),
|
||||
description: "Use for SDK implementation details",
|
||||
source,
|
||||
}),
|
||||
])
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(Reference.layer),
|
||||
Effect.provide(cache),
|
||||
Effect.provide(EventV2.defaultLayer),
|
||||
Effect.provide(Global.defaultLayer),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@ -10,7 +10,27 @@ import { testEffect } from "./lib/effect"
|
||||
const it = testEffect(Ripgrep.defaultLayer)
|
||||
|
||||
describe("Ripgrep", () => {
|
||||
it.live("allows caller globs to re-include git metadata", () =>
|
||||
it.live("keeps ignored files out of catch-all find results", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true }))
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
|
||||
yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`)
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n"))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n"))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n"))
|
||||
|
||||
const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 })
|
||||
expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js"))
|
||||
expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js"))
|
||||
}),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("never includes git metadata", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
@ -23,7 +43,7 @@ describe("Ripgrep", () => {
|
||||
|
||||
const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 })
|
||||
expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
|
||||
expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config"))
|
||||
expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config"))
|
||||
|
||||
const observed: string[] = []
|
||||
const limited = yield* ripgrep.find({
|
||||
@ -36,7 +56,7 @@ describe("Ripgrep", () => {
|
||||
|
||||
const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
|
||||
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
|
||||
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config"))
|
||||
expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config"))
|
||||
}),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
),
|
||||
|
||||
@ -25,6 +25,7 @@ import { Location } from "@opencode-ai/core/location"
|
||||
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
@ -70,6 +71,7 @@ const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
|
||||
const systemContext = SystemContextRegistry.layer
|
||||
const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||
const runner = SessionRunnerLLM.defaultLayer.pipe(
|
||||
Layer.provide(database),
|
||||
@ -82,6 +84,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
|
||||
Layer.provide(location),
|
||||
Layer.provide(agents),
|
||||
Layer.provide(skillGuidance),
|
||||
Layer.provide(referenceGuidance),
|
||||
Layer.provide(config),
|
||||
)
|
||||
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
||||
|
||||
@ -48,6 +48,7 @@ import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
@ -215,6 +216,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
||||
: SystemContext.empty,
|
||||
),
|
||||
})
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
@ -243,6 +245,7 @@ const runner = SessionRunnerLLM.layer.pipe(
|
||||
Layer.provide(location),
|
||||
Layer.provide(agents),
|
||||
Layer.provide(skillGuidance),
|
||||
Layer.provide(referenceGuidance),
|
||||
Layer.provide(config),
|
||||
)
|
||||
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
||||
|
||||
@ -24,9 +24,13 @@ import { Effect, Context, Layer, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
name: Schema.String,
|
||||
@ -89,15 +93,21 @@ export const layer = Layer.effect(
|
||||
const plugin = yield* Plugin.Service
|
||||
const skill = yield* Skill.Service
|
||||
const provider = yield* Provider.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Agent.state")(function* (ctx) {
|
||||
const cfg = yield* config.get()
|
||||
const skillDirs = yield* skill.dirs()
|
||||
const referenceDirs = yield* Effect.gen(function* () {
|
||||
yield* (yield* PluginBoot.Service).wait()
|
||||
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
|
||||
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
||||
const whitelistedDirs = [
|
||||
Truncate.GLOB,
|
||||
path.join(Global.Path.tmp, "*"),
|
||||
...skillDirs.map((dir) => path.join(dir, "*")),
|
||||
...referenceDirs.map((dir) => path.join(dir, "*")),
|
||||
]
|
||||
const readonlyExternalDirectory = {
|
||||
"*": "ask",
|
||||
@ -429,8 +439,18 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node])
|
||||
const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, [])
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Config.node,
|
||||
Auth.node,
|
||||
Plugin.node,
|
||||
Skill.node,
|
||||
Provider.node,
|
||||
locationServiceMapNode,
|
||||
])
|
||||
|
||||
export * as Agent from "./agent"
|
||||
|
||||
@ -2,13 +2,14 @@ import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(
|
||||
Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })),
|
||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { EOL } from "os"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
@ -37,9 +38,11 @@ export const V2Command = effectCmd({
|
||||
}).pipe(
|
||||
Effect.withSpan("Cli.debug.v2"),
|
||||
Effect.provide(
|
||||
LocationServiceMap.get({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
}),
|
||||
LocationServiceMap.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
),
|
||||
|
||||
@ -18,7 +18,9 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
|
||||
|
||||
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { handlePtyInput } from "@opencode-ai/core/pty/input"
|
||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
@ -41,13 +42,15 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
|
||||
const cors = yield* CorsConfig
|
||||
const locations = yield* LocationServiceMap
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unregister))
|
||||
|
||||
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@ -158,13 +161,15 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
||||
const cors = yield* CorsConfig
|
||||
const locations = yield* LocationServiceMap
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unregister))
|
||||
|
||||
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -16,6 +16,11 @@ import type { Provider } from "@/provider/provider"
|
||||
import type { Agent } from "@/agent/agent"
|
||||
import { Permission } from "@/permission"
|
||||
import { Skill } from "@/skill"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
|
||||
export function provider(model: Provider.Model) {
|
||||
if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
||||
@ -44,10 +49,15 @@ export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
return Service.of({
|
||||
environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const references = yield* Effect.gen(function* () {
|
||||
yield* (yield* PluginBoot.Service).wait()
|
||||
return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined)
|
||||
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
||||
return [
|
||||
[
|
||||
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
|
||||
@ -60,7 +70,25 @@ export const layer = Layer.effect(
|
||||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
].join("\n"),
|
||||
]
|
||||
references.length === 0
|
||||
? undefined
|
||||
: [
|
||||
"Project references provide additional directories that can be accessed when relevant.",
|
||||
"<available_references>",
|
||||
...references
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.flatMap((reference) => [
|
||||
" <reference>",
|
||||
` <name>${reference.name}</name>`,
|
||||
` <path>${reference.path}</path>`,
|
||||
...(reference.description === undefined
|
||||
? []
|
||||
: [` <description>${reference.description}</description>`]),
|
||||
" </reference>",
|
||||
]),
|
||||
"</available_references>",
|
||||
].join("\n"),
|
||||
].filter((part): part is string => part !== undefined)
|
||||
}),
|
||||
|
||||
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
|
||||
@ -80,8 +108,10 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer), Layer.provide(LocationServiceMap.layer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Skill.node])
|
||||
const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, [])
|
||||
|
||||
export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode])
|
||||
|
||||
export * as SystemPrompt from "./system"
|
||||
|
||||
@ -14,6 +14,7 @@ import { Plugin } from "../../src/plugin"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
|
||||
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Agent.layer.pipe(
|
||||
@ -22,6 +23,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
)
|
||||
|
||||
@ -119,7 +121,7 @@ it.instance(
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
reference: {
|
||||
references: {
|
||||
effect: "github.com/effect/effect-smol",
|
||||
effectFull: {
|
||||
repository: "Effect-TS/effect",
|
||||
@ -601,6 +603,25 @@ description: Permission skill.
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"project reference directories are allowed for external_directory",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const build = yield* load((svc) => svc.get("build"))
|
||||
const target = path.resolve(test.directory, "../docs/reference/notes.md")
|
||||
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
||||
}),
|
||||
{
|
||||
git: true,
|
||||
config: {
|
||||
references: {
|
||||
docs: "../docs",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance("defaultAgent returns build when no default_agent config", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* load((svc) => svc.defaultAgent())
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import path from "path"
|
||||
@ -43,6 +44,7 @@ const agentLayer = Agent.layer.pipe(
|
||||
Layer.provide(SkillTest.empty),
|
||||
Layer.provide(provider.layer),
|
||||
Layer.provide(pluginLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||
)
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ describe("reference HttpApi", () => {
|
||||
config: {
|
||||
formatter: false,
|
||||
lsp: false,
|
||||
reference: {
|
||||
references: {
|
||||
docs: "./docs",
|
||||
effect: { repository: "Effect-TS/effect", branch: "main" },
|
||||
bad: "not-a-repo",
|
||||
@ -35,18 +35,26 @@ describe("reference HttpApi", () => {
|
||||
{
|
||||
name: "docs",
|
||||
path: path.join(tmp.path, "docs"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
source: {
|
||||
type: "local",
|
||||
path: path.join(tmp.path, "docs"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "effect",
|
||||
path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
source: {
|
||||
type: "git",
|
||||
repository: "Effect-TS/effect",
|
||||
branch: "main",
|
||||
description: null,
|
||||
hidden: null,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@ -5,6 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const skills: Skill.Info[] = [
|
||||
@ -42,6 +43,7 @@ const build: Agent.Info = {
|
||||
|
||||
const it = testEffect(
|
||||
SystemPrompt.layer.pipe(
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(
|
||||
Layer.succeed(
|
||||
Skill.Service,
|
||||
|
||||
@ -1685,26 +1685,6 @@ export type ServerConfig = {
|
||||
cors?: Array<string>
|
||||
}
|
||||
|
||||
export type ReferenceConfigEntry =
|
||||
| string
|
||||
| {
|
||||
/**
|
||||
* Git repository URL, host/path reference, or GitHub owner/repo shorthand
|
||||
*/
|
||||
repository: string
|
||||
branch?: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Absolute path, ~/ path, or workspace-relative path to a local reference directory
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
export type ReferenceConfig = {
|
||||
[key: string]: ReferenceConfigEntry
|
||||
}
|
||||
|
||||
export type PermissionActionConfig = "ask" | "allow" | "deny"
|
||||
|
||||
export type PermissionObjectConfig = {
|
||||
@ -1948,7 +1928,9 @@ export type Config = {
|
||||
paths?: Array<string>
|
||||
urls?: Array<string>
|
||||
}
|
||||
reference?: ReferenceConfig
|
||||
references?: {
|
||||
[key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal
|
||||
}
|
||||
watcher?: {
|
||||
ignore?: Array<string>
|
||||
}
|
||||
@ -3723,6 +3705,19 @@ export type SyncEventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type ConfigV2ReferenceGit = {
|
||||
repository: string
|
||||
branch?: string
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type ConfigV2ReferenceLocal = {
|
||||
path: string
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type PolicyEffect = "allow" | "deny"
|
||||
|
||||
export type ConfigV2ExperimentalPolicy = {
|
||||
@ -4174,17 +4169,23 @@ export type QuestionV2Reply = {
|
||||
export type ReferenceLocalSource = {
|
||||
type: "local"
|
||||
path: string
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type ReferenceGitSource = {
|
||||
type: "git"
|
||||
repository: string
|
||||
branch?: string
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type ReferenceInfo = {
|
||||
name: string
|
||||
path: string
|
||||
description?: string
|
||||
hidden?: boolean
|
||||
source: ReferenceLocalSource | ReferenceGitSource
|
||||
}
|
||||
|
||||
|
||||
@ -17628,44 +17628,6 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Server configuration for opencode serve and web commands"
|
||||
},
|
||||
"ReferenceConfigEntry": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"description": "Git repository URL, host/path reference, or GitHub owner/repo shorthand"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["repository"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path, ~/ path, or workspace-relative path to a local reference directory"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReferenceConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/ReferenceConfigEntry"
|
||||
}
|
||||
},
|
||||
"PermissionActionConfig": {
|
||||
"type": "string",
|
||||
"enum": ["ask", "allow", "deny"]
|
||||
@ -18256,8 +18218,21 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"reference": {
|
||||
"$ref": "#/components/schemas/ReferenceConfig"
|
||||
"references": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ConfigV2ReferenceGit"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ConfigV2ReferenceLocal"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"watcher": {
|
||||
"type": "object",
|
||||
@ -23918,6 +23893,41 @@
|
||||
"required": ["type", "id", "syncEvent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ConfigV2ReferenceGit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["repository"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ConfigV2ReferenceLocal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PolicyEffect": {
|
||||
"type": "string",
|
||||
"enum": ["allow", "deny"]
|
||||
@ -25190,6 +25200,12 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["type", "path"],
|
||||
@ -25207,6 +25223,12 @@
|
||||
},
|
||||
"branch": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["type", "repository"],
|
||||
@ -25221,6 +25243,12 @@
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@ -57,12 +57,12 @@ export class LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
const query = new URL(request.url, "http://localhost").searchParams
|
||||
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
||||
return {
|
||||
return Location.Ref.make({
|
||||
directory: AbsolutePath.make(
|
||||
query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
|
||||
),
|
||||
workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
@ -54,10 +55,12 @@ export const sessionLocationLayer = Layer.effect(
|
||||
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(
|
||||
locations.get({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
locations.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
||||
@ -278,7 +278,7 @@ export function Autocomplete(props: {
|
||||
const { baseQuery } = extractLineRange(search())
|
||||
const slash = baseQuery.indexOf("/")
|
||||
const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash)
|
||||
return syncV2.data.reference.find((item) => item.name === alias)
|
||||
return syncV2.data.reference.find((item) => !item.hidden && item.name === alias)
|
||||
})
|
||||
|
||||
function normalizeMentionPath(filePath: string) {
|
||||
@ -411,25 +411,27 @@ export function Autocomplete(props: {
|
||||
})
|
||||
|
||||
const referenceAliases = createMemo(() =>
|
||||
syncV2.data.reference.map(
|
||||
(reference): AutocompleteOption => ({
|
||||
display: "@" + reference.name,
|
||||
description: " dir",
|
||||
onSelect: () => {
|
||||
insertPart(reference.name, {
|
||||
type: "file",
|
||||
mime: "application/x-directory",
|
||||
filename: reference.name,
|
||||
url: pathToFileURL(reference.path).href,
|
||||
source: {
|
||||
syncV2.data.reference
|
||||
.filter((reference) => !reference.hidden)
|
||||
.map(
|
||||
(reference): AutocompleteOption => ({
|
||||
display: "@" + reference.name,
|
||||
description: ` ${reference.source.type === "git" ? reference.source.repository : reference.source.path}`,
|
||||
onSelect: () => {
|
||||
insertPart(reference.name, {
|
||||
type: "file",
|
||||
text: { start: 0, end: 0, value: "" },
|
||||
path: reference.name,
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
mime: "application/x-directory",
|
||||
filename: reference.name,
|
||||
url: pathToFileURL(reference.path).href,
|
||||
source: {
|
||||
type: "file",
|
||||
text: { start: 0, end: 0, value: "" },
|
||||
path: reference.name,
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const commands = createMemo((): AutocompleteOption[] => {
|
||||
|
||||
@ -264,6 +264,7 @@ export default defineConfig({
|
||||
"mcp-servers",
|
||||
"acp",
|
||||
"skills",
|
||||
"references",
|
||||
"custom-tools",
|
||||
],
|
||||
},
|
||||
|
||||
157
packages/web/src/content/docs/references.mdx
Normal file
157
packages/web/src/content/docs/references.mdx
Normal file
@ -0,0 +1,157 @@
|
||||
---
|
||||
title: References
|
||||
description: Add local directories and Git repositories as project references.
|
||||
---
|
||||
|
||||
References give OpenCode access to directories outside the current project. Use them to make documentation, shared libraries, examples, or another repository available while you work.
|
||||
|
||||
References are configured by alias in `opencode.json` or `opencode.jsonc`.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"references": {
|
||||
"docs": {
|
||||
"path": "../product-docs",
|
||||
"description": "Use for product behavior and documentation conventions",
|
||||
},
|
||||
"sdk": {
|
||||
"repository": "anomalyco/opencode-sdk-js",
|
||||
"branch": "main",
|
||||
"description": "Use for JavaScript SDK implementation details",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local directories
|
||||
|
||||
Use `path` to reference a local directory.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"docs": {
|
||||
"path": "../docs",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Paths can be:
|
||||
|
||||
- Relative to the config file that defines the reference
|
||||
- Absolute, such as `/home/user/docs`
|
||||
- Relative to your home directory, such as `~/docs`
|
||||
|
||||
You can also use a string shorthand:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"docs": "../docs",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git repositories
|
||||
|
||||
Use `repository` to reference a Git repository. OpenCode materializes the repository in its local repository cache and makes the checked-out source available as a reference directory.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"effect": {
|
||||
"repository": "Effect-TS/effect",
|
||||
"branch": "main",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`repository` accepts Git URLs, host/path references, and GitHub `owner/repo` shorthand. The optional `branch` field selects a branch or ref. Without `branch`, OpenCode uses the repository's default branch.
|
||||
|
||||
You can use string shorthand when you do not need a branch, description, or other options:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"effect": "Effect-TS/effect",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
Git references are refreshed asynchronously. A newly configured repository may take a moment to finish cloning or updating.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Describe usage
|
||||
|
||||
Add `description` to explain when an agent should use a reference.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"design-system": {
|
||||
"path": "../design-system",
|
||||
"description": "Use when implementing UI components or design tokens",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode includes references with descriptions in agent context. Descriptions should be short and specific enough to distinguish references with similar content. References without descriptions remain available through autocomplete and direct use, but are not advertised to agents.
|
||||
|
||||
---
|
||||
|
||||
## Hide autocomplete entries
|
||||
|
||||
Set `hidden` to `true` to omit a reference from `@` autocomplete in the TUI.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"references": {
|
||||
"internal": {
|
||||
"path": "../internal",
|
||||
"description": "Use for internal implementation details",
|
||||
"hidden": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`hidden` only affects autocomplete. A hidden reference with a description remains included in agent context.
|
||||
|
||||
---
|
||||
|
||||
## Use references
|
||||
|
||||
Configured references appear in TUI `@` autocomplete. Type `@alias` to attach the reference root, or `@alias/` to search for files inside it.
|
||||
|
||||
```text
|
||||
Compare this implementation with @sdk/src/client.ts
|
||||
```
|
||||
|
||||
Agents also receive the resolved paths and descriptions of configured references that have descriptions in their system context, so they can inspect a reference when it is relevant without you attaching it manually.
|
||||
|
||||
OpenCode automatically allows reference directories through its external-directory permission boundary. Normal tool permissions still apply; for example, an agent that cannot edit files does not gain edit access because a directory is configured as a reference.
|
||||
|
||||
---
|
||||
|
||||
## Configure fields
|
||||
|
||||
| Field | Local | Git | Description |
|
||||
| ------------- | ----- | --- | ------------------------------------------------ |
|
||||
| `path` | Yes | No | Local reference directory |
|
||||
| `repository` | No | Yes | Git URL, host/path, or GitHub `owner/repo` value |
|
||||
| `branch` | No | Yes | Optional Git branch or ref |
|
||||
| `description` | Yes | Yes | Guidance describing when to use the reference |
|
||||
| `hidden` | Yes | Yes | Hide the reference from TUI `@` autocomplete |
|
||||
|
||||
Reference aliases cannot be empty or contain `/`, whitespace, backticks, or commas.
|
||||
@ -41,7 +41,7 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
||||
|
||||
The content of the file is added to the conversation automatically.
|
||||
|
||||
Configured references also appear in `@` autocomplete. Type `@alias` to add the reference root as context, or type `@alias/` to autocomplete files inside that reference.
|
||||
Configured [references](/docs/references) also appear in `@` autocomplete. Type `@alias` to add the reference root as context, or type `@alias/` to autocomplete files inside that reference.
|
||||
|
||||
```text "@docs/README.md"
|
||||
Compare our setup with @docs/README.md
|
||||
|
||||
Loading…
Reference in New Issue
Block a user