export * as PluginHooks from "./hooks.js" import type { AISDKHooks } from "@opencode/plugin/effect/aisdk" import type { SessionHooks } from "@opencode/plugin/effect/session" import type { ShellHooks } from "@opencode/plugin/effect/shell" import type { ToolFailures, ToolHooks } from "@opencode/plugin/effect/tool" import type { ModelHookOptions } from "@opencode/plugin/effect/registration" import type { PermissionHooks } from "@opencode/plugin/effect/permission" import { Context, Effect, Layer, Scope } from "effect" import { makeLocationNode } from "@opencode/util/effect/app-node" import { State } from "../state.js" export interface Domains { readonly aisdk: AISDKHooks readonly session: SessionHooks readonly permission: PermissionHooks readonly shell: ShellHooks readonly tool: ToolHooks } type NoFailures = { readonly [Name in keyof Spec]: never } // Failure channel for each hook event. Only tool execute.before may fail: a Tool.Error rejects the call before it runs. interface Failures extends Record { readonly aisdk: NoFailures readonly session: NoFailures readonly permission: NoFailures readonly shell: NoFailures readonly tool: ToolFailures } type Callback = (event: Event) => Effect.Effect type Entry = { readonly callback: Function; readonly options?: ModelHookOptions } const eventProviderID = (event: unknown) => { if (typeof event !== "object" || event === null || !("model" in event)) return undefined const model = event.model if (typeof model !== "object" || model === null || !("providerID" in model)) return undefined return typeof model.providerID === "string" ? model.providerID : undefined } export interface Interface { readonly has: ( domain: Domain, name: keyof Domains[Domain] & keyof Failures[Domain], providerID?: string, ) => Effect.Effect readonly register: ( domain: Domain, name: Name, callback: Callback, options?: ModelHookOptions, ) => Effect.Effect readonly trigger: ( domain: Domain, name: Name, event: Domains[Domain][Name], ) => Effect.Effect } export class Service extends Context.Service()("@opencode/PluginHooks") {} const layer = Layer.effect( Service, Effect.gen(function* () { const callbacks = new Map() const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}` const register: Interface["register"] = Effect.fn("PluginHooks.register")( function* (domain, name, callback, options) { const scope = yield* Scope.Scope const id = key(domain, name) let active = true const entry = { callback, options } callbacks.set(id, [...(callbacks.get(id) ?? []), entry]) const dispose = Effect.sync(() => { if (!active) return active = false const next = (callbacks.get(id) ?? []).filter((item) => item !== entry) if (next.length === 0) callbacks.delete(id) else callbacks.set(id, next) }) yield* Scope.addFinalizer(scope, dispose) return { dispose } }, ) const trigger: Interface["trigger"] = Effect.fnUntraced(function* (domain, name, event) { for (const entry of callbacks.get(key(domain, name)) ?? []) { if (entry.options?.providerID !== undefined && entry.options.providerID !== eventProviderID(event)) continue const result: Effect.Effect = entry.callback(event) yield* result } return event }) const has: Interface["has"] = (domain, name, providerID) => Effect.sync(() => (callbacks.get(key(domain, name)) ?? []).some( (entry) => entry.options?.providerID === undefined || entry.options.providerID === providerID, ), ) return Service.of({ has, register, trigger }) }), ) export const node = makeLocationNode({ service: Service, layer, deps: [] })