mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-09 22:25:02 +03:00
fix(core): reload compatibility skills only for their own sources (#59086)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
01cd723db9
commit
eba3399588
@@ -20,6 +20,8 @@ export const Plugin = define({
|
||||
const changes = yield* PubSub.sliding<string>(1)
|
||||
const lock = Semaphore.makeUnsafe(1)
|
||||
const skills: Skill.Info[] = []
|
||||
const compatibility = () =>
|
||||
config.compatibility ? config.compatibility() : Effect.succeed({ claude: [], agents: [] })
|
||||
|
||||
const watch = Effect.fn("ConfigCompatibilityPlugin.watch")(function* (
|
||||
target: string,
|
||||
@@ -39,7 +41,7 @@ export const Plugin = define({
|
||||
const refresh = Effect.fn("ConfigCompatibilityPlugin.refresh")(
|
||||
function* () {
|
||||
yield* FiberMap.clear(watches)
|
||||
const roots = config.compatibility ? yield* config.compatibility() : { claude: [], agents: [] }
|
||||
const roots = yield* compatibility()
|
||||
const directories = [...roots.claude, ...roots.agents].map((root) => path.join(root, "skills"))
|
||||
const loaded = new Map<Skill.ID, Skill.Info>()
|
||||
for (const directory of directories) {
|
||||
@@ -66,7 +68,10 @@ export const Plugin = define({
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* config.changes().pipe(
|
||||
Stream.runForEach(() => reload),
|
||||
Stream.filterEffect((update) =>
|
||||
Effect.map(compatibility(), (roots) => isCompatibilitySource(roots, update.path)),
|
||||
),
|
||||
Stream.runForEach((update) => PubSub.publish(changes, update.path).pipe(Effect.asVoid)),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* refresh()
|
||||
@@ -75,3 +80,15 @@ export const Plugin = define({
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
const sourceDirectories = [".claude", ".agents"] as const
|
||||
|
||||
function isCompatibilitySource(
|
||||
roots: { readonly claude: readonly string[]; readonly agents: readonly string[] },
|
||||
file: string,
|
||||
) {
|
||||
return (
|
||||
sourceDirectories.some((name) => path.basename(file) === name) ||
|
||||
[...roots.claude, ...roots.agents].some((root) => FSUtil.contains(root, file))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Skill } from "@opencode/core/skill"
|
||||
import { SkillDiscovery } from "@opencode/core/skill/discovery"
|
||||
import { WellKnown } from "@opencode/core/wellknown"
|
||||
import { emptyCredentialNode, emptyWellknownNode } from "../fixture/config-nodes"
|
||||
import { advance, drain } from "../lib/clock"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -427,4 +428,53 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("ignores unrelated config updates and coalesces compatibility bursts into one reload", () =>
|
||||
Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const claude = path.join(tmp.path, ".claude")
|
||||
const skills = path.join(claude, "skills")
|
||||
const service = yield* Skill.Service
|
||||
const configTest = yield* Config.Test
|
||||
let reloads = 0
|
||||
yield* ConfigCompatibilityPlugin.Plugin.effect(
|
||||
host({
|
||||
skill: {
|
||||
list: () => Effect.die("unused skill.list"),
|
||||
transform: service.transform,
|
||||
reload: () => service.reload().pipe(Effect.tap(() => Effect.sync(() => reloads++))),
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* configTest.emitChange({ type: "update", path: path.join(tmp.path, "opencode.json") })
|
||||
yield* configTest.emitChange({ type: "create", path: path.join(tmp.path, "commands", "review.md") })
|
||||
yield* configTest.emitChange({ type: "create", path: path.join(tmp.path, "agents", "reviewer.md") })
|
||||
yield* drain
|
||||
expect(reloads).toBe(0)
|
||||
|
||||
yield* Effect.promise(async () => {
|
||||
await fs.mkdir(path.join(skills, "review"), { recursive: true })
|
||||
await write(skills, "review", "Review once")
|
||||
})
|
||||
yield* configTest.emitChange({ type: "create", path: claude })
|
||||
yield* configTest.emitChange({ type: "create", path: path.join(skills, "review", "SKILL.md") })
|
||||
yield* configTest.emitChange({ type: "update", path: path.join(skills, "review", "SKILL.md") })
|
||||
yield* advance(() => reloads >= 1)
|
||||
yield* drain
|
||||
expect(reloads).toBe(1)
|
||||
expect((yield* service.list()).find((item) => item.id === "review")?.description).toBe("Review once")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
Config.testLayer(
|
||||
[new Directory({ type: "directory", path: AbsolutePath.make(tmp.path) })],
|
||||
{ claude: [AbsolutePath.make(path.join(tmp.path, ".claude"))], agents: [] },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user