Compare commits

..
Author SHA1 Message Date
Kit Langton c35913fd2b chore(core): add plugin interrupt changeset 2026-08-27 16:04:00 -04:00
Kit Langton 3d9ad3b183 fix(core): forward plugin interrupt options 2026-08-27 15:08:48 -04:00
6 changed files with 39 additions and 50 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@opencode-ai/core": patch
---
Forward plugin Session interrupt options so continuation requests reach the Session service.
-5
View File
@@ -1,5 +0,0 @@
---
"@opencode-ai/core": patch
---
Preserve reference insertion order when later config documents override an existing reference.
+3 -1
View File
@@ -20,13 +20,14 @@ export const Plugin = define({
const global = yield* Global.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.reference.reload())
yield* ctx.reference.transform((draft) => {
const entries = new Map<string, Reference.Source>()
for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
const description = typeof entry === "string" ? undefined : entry.description
const hidden = typeof entry === "string" ? undefined : entry.hidden
draft.add(
entries.set(
name,
local(entry)
? Reference.LocalSource.make({
@@ -47,6 +48,7 @@ export const Plugin = define({
)
}
}
for (const [name, source] of entries) draft.add(name, source)
})
}),
})
+1 -1
View File
@@ -76,7 +76,7 @@ export const layerWithCell = (cell: Cell) =>
resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)),
switchAgent: (input) => require(cell, (runtime) => runtime.session.switchAgent(input)),
switchModel: (input) => require(cell, (runtime) => runtime.session.switchModel(input)),
interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)),
interrupt: (sessionID, options) => require(cell, (runtime) => runtime.session.interrupt(sessionID, options)),
synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)),
wait: (sessionID) => require(cell, (runtime) => runtime.session.wait(sessionID)),
context: (sessionID) => require(cell, (runtime) => runtime.session.context(sessionID)),
-43
View File
@@ -34,41 +34,6 @@ const decode = Schema.decodeUnknownSync(Info)
const document = path.join(import.meta.dir, "opencode.json")
describe("config plugin reloads", () => {
it.effect("preserves reference precedence and insertion order across documents", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const references = yield* Reference.Service
const host = yield* PluginHost.make(plugins)
yield* references.transform((draft) =>
draft.add(
"external",
Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/references/external") }),
),
)
yield* ConfigReferencePlugin.Plugin.effect(host)
const result = yield* references.list()
expect(result.map((reference) => reference.name)).toEqual(["external", "shared", "first", "second"])
expect(result.find((reference) => reference.name === "shared")?.path).toBe(
AbsolutePath.make(path.resolve("/config/second/shared")),
)
}).pipe(
Effect.provide(
Config.testLayer([
referenceConfig("/config/first/opencode.json", {
shared: "./shared",
first: "./first",
}),
referenceConfig("/config/second/opencode.json", {
shared: "./shared",
second: "./second",
}),
]),
),
Effect.provideService(Global.Service, Global.Service.of(Global.make())),
),
)
it.live("reloads config-backed domains without reloading external plugins", () =>
Effect.gen(function* () {
const agents = yield* Agent.Service
@@ -137,14 +102,6 @@ function config(name: string) {
})
}
function referenceConfig(file: string, references: Record<string, string>) {
return new Document({
type: "document",
path: AbsolutePath.make(file),
info: decode({ references }),
})
}
function title(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
+30
View File
@@ -113,6 +113,36 @@ describe("Plugin", () => {
}),
)
it.effect("forwards session interrupt options through the runtime cell", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
const fallback = yield* PluginRuntime.Service
const cell = PluginRuntime.makeCell()
const sessionID = Session.ID.create()
const calls: Array<{ sessionID: Session.ID; options: { continue?: boolean } | undefined }> = []
cell.runtime = {
...fallback,
session: {
...fallback.session,
interrupt: (id, options) =>
Effect.sync(() => {
calls.push({ sessionID: id, options })
return true
}),
},
}
const runtime = yield* PluginRuntime.Service.pipe(Effect.provide(PluginRuntime.layerWithCell(cell)))
const host = yield* PluginHost.make(plugins).pipe(Effect.provideService(PluginRuntime.Service, runtime))
expect(yield* runtime.session.interrupt(sessionID)).toBe(true)
expect(yield* host.session.interrupt({ sessionID, continue: true })).toEqual({ interrupted: true })
expect(calls).toEqual([
{ sessionID, options: undefined },
{ sessionID, options: { continue: true } },
])
}),
)
it.effect("registers and removes scoped VCS providers", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service