feat(plugin): expose the current location in plugin context (#45403)

Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-08-26 19:09:16 -04:00 committed by GitHub
parent 6b0613ac6f
commit 3184427c03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 98 additions and 0 deletions

View file

@ -73,6 +73,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: Interface, p
return {
app,
location: locationInfo(),
options: {},
agent: {
get: (input) => {

View file

@ -25,6 +25,34 @@ class Secret extends Context.Service<Secret, string>()("@opencode/test/PluginSec
const versioned = <R>(plugin: EffectPlugin.Plugin<R>, version = "1") => ({ ...plugin, version })
describe("Plugin", () => {
it.effect("exposes the current location to activated plugins", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const location = yield* Location.Service
const seen: Location.Info[] = []
yield* plugins.activate([
versioned(
EffectPlugin.define({
id: "location-context",
effect: (ctx) =>
Effect.sync(() => {
seen.push(ctx.location)
}),
}),
"1",
),
])
expect(seen).toEqual([
new Location.Info({
directory: location.directory,
workspaceID: location.workspaceID,
project: location.project,
}),
])
}),
)
it.live("exposes public events through the plugin context", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service

View file

@ -18,6 +18,16 @@ type Overrides = Partial<Omit<Plugin.Context, "options" | "session">> & {
export function host(overrides: Overrides = {}): Plugin.Context {
return {
app: overrides.app ?? { name: "test", version: "test", channel: "test" },
location:
overrides.location ??
new Location.Info({
directory: AbsolutePath.make("/workspace"),
project: {
id: Project.ID.global,
directory: AbsolutePath.make("/workspace"),
canonical: AbsolutePath.make("/workspace"),
},
}),
options: {},
agent: overrides.agent ?? {
get: () => Effect.die("unused agent.get"),

View file

@ -17,6 +17,7 @@ import { SessionInbox } from "@opencode-ai/core/session/inbox"
import { Tool } from "@opencode-ai/core/tool"
import { Provider } from "@opencode-ai/core/provider"
import { Project } from "@opencode-ai/core/project"
import { Workspace } from "@opencode-ai/core/workspace"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { define } from "@opencode-ai/plugin/promise/plugin"
import { Money } from "@opencode-ai/schema/money"
@ -28,6 +29,41 @@ import { host as testHost } from "./host"
const it = testEffect(PluginTestLayer)
describe("fromPromise", () => {
it.effect("exposes the host location including workspace and project metadata", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const location = yield* Location.Service
const expected = new Location.Info({
directory: AbsolutePath.make("/worktree/packages/app"),
workspaceID: Workspace.ID.make("wrk_plugin_location"),
project: {
id: Project.ID.global,
directory: AbsolutePath.make("/worktree"),
canonical: AbsolutePath.make("/project"),
},
})
const host = yield* PluginHost.make(plugins).pipe(
Effect.provideService(Location.Service, {
...location,
directory: expected.directory,
workspaceID: expected.workspaceID,
project: expected.project,
}),
)
const seen: Location.Info[] = []
yield* PluginPromise.fromPromise(
define({
id: "promise-location",
setup: (ctx) => {
seen.push(ctx.location)
},
}),
).effect(host)
expect(seen).toEqual([expected])
}),
)
it.effect("adapts plugin storage methods", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service

View file

@ -6,6 +6,7 @@ export { Command } from "@opencode-ai/schema/command"
export { Connection } from "@opencode-ai/schema/connection"
export { Credential } from "@opencode-ai/schema/credential"
export { Integration } from "@opencode-ai/schema/integration"
export { Location } from "@opencode-ai/schema/location"
export { Mcp } from "@opencode-ai/schema/mcp"
export { Model } from "@opencode-ai/schema/model"
export { Provider } from "@opencode-ai/schema/provider"

View file

@ -1,4 +1,5 @@
import type { GenerateApi, PluginApi } from "@opencode-ai/client/effect/api"
import type { Location } from "@opencode-ai/schema/location"
import type { Effect, Scope } from "effect"
import type { PluginOptions } from "../options.js"
import type { VcsDiscovery } from "../vcs.js"
@ -22,6 +23,7 @@ import type { WebSearchDomain } from "./websearch.js"
export interface Context {
readonly app: App
readonly location: Location.Info
readonly options: PluginOptions
readonly agent: AgentDomain
readonly aisdk: AISDKDomain

View file

@ -126,6 +126,7 @@ export function fromPromise(plugin: Plugin) {
const context2: Context = {
app: host.app,
location: host.location,
options: host.options,
agent: {
get: adaptApiMethod(AgentEndpoints["agent.get"], host.agent.get),

View file

@ -7,6 +7,7 @@ export { Command } from "@opencode-ai/schema/command"
export { Connection } from "@opencode-ai/schema/connection"
export { Credential } from "@opencode-ai/schema/credential"
export { Integration } from "@opencode-ai/schema/integration"
export { Location } from "@opencode-ai/schema/location"
export { Mcp } from "@opencode-ai/schema/mcp"
export { Model } from "@opencode-ai/schema/model"
export { Provider } from "@opencode-ai/schema/provider"

View file

@ -1,4 +1,5 @@
import type { GenerateApi, PluginApi } from "@opencode-ai/client/promise/api"
import type { Location } from "@opencode-ai/schema/location"
import type { PluginOptions } from "../options.js"
import type { VcsDiscovery } from "../vcs.js"
import type { App } from "../app.js"
@ -21,6 +22,7 @@ import type { WebSearchDomain } from "./websearch.js"
export interface Context {
readonly app: App
readonly location: Location.Info
readonly options: PluginOptions
readonly agent: AgentDomain
readonly aisdk: AISDKDomain

View file

@ -5,6 +5,7 @@ import { Command } from "@opencode-ai/schema/command"
import { Connection } from "@opencode-ai/schema/connection"
import { Credential } from "@opencode-ai/schema/credential"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
import { Mcp } from "@opencode-ai/schema/mcp"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
@ -26,6 +27,7 @@ test.each([
expect(entrypoint.Connection).toBe(Connection)
expect(entrypoint.Credential).toBe(Credential)
expect(entrypoint.Integration).toBe(Integration)
expect(entrypoint.Location).toBe(Location)
expect(entrypoint.Mcp).toBe(Mcp)
expect(entrypoint.Model).toBe(Model)
expect(entrypoint.Provider).toBe(Provider)
@ -39,6 +41,7 @@ test.each([
"Connection",
"Credential",
"Integration",
"Location",
"Mcp",
"Model",
"Plugin",

View file

@ -72,6 +72,19 @@ setup(ctx) {
}
```
`ctx.location` describes the location where this plugin instance is loaded. It
includes `directory`, optional `workspaceID`, and `project` metadata (`id`,
`directory`, and `canonical`). It is available in both Promise and Effect plugins.
This is the plugin instance's location, not the location of every session it can
access or event it receives.
```ts
setup(ctx) {
console.log(ctx.location.directory)
console.log(ctx.location.project.canonical)
}
```
### Options
Pass plugin options with the object form in `opencode.json(c)`.