From 7831f171d1c7aa33b03a180d6288ef95535267a5 Mon Sep 17 00:00:00 2001 From: filipeforattini Date: Fri, 21 Aug 2026 08:49:15 -0300 Subject: [PATCH] feat(opencode): V2 commands.pre-execute alongside V1 trigger Adds `SessionEvent.Command.PreExecute` (live waterfall event, type `session.next.command.pre_execute`) carrying `{ command, sessionID, arguments, parts }` and wires `EventV2Bridge.waterfall` into the command dispatch next to the existing V1 `plugin.trigger('command.execute.before', ...)`. The waterfall currently has a single listener that delegates to the V1 trigger so existing plugins keep working. Listeners return `{ parts }` to rewrite the prompt payload; throwing short-circuits the command. Same shim pattern as #70 #72. --- packages/opencode/src/session/prompt.ts | 11 ++++++++++- packages/schema/src/session-event.ts | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index e076a9c246ef..ade75c66c529 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1452,7 +1452,7 @@ const layer = Layer.effect( (part) => part.type !== "file" || !inputFiles.has(fileURLToPath(part.url)), ) const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true - const parts = isSubtask + let parts = isSubtask ? [ { type: "subtask" as const, @@ -1477,6 +1477,15 @@ const layer = Layer.effect( { command: input.command, sessionID: input.sessionID, arguments: input.arguments }, { parts }, ) + const decidedCommand = yield* events.waterfall(SessionEvent.Command.PreExecute, [ + (_e, next) => + Effect.gen(function* () { + const downstream = yield* next() + const downstreamParts = (downstream as { parts?: typeof parts } | undefined)?.parts + return { parts: downstreamParts ?? parts } + }), + ]) + parts = (decidedCommand as { parts: typeof parts }).parts const result = yield* prompt({ sessionID: input.sessionID, diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 4ffb70ff7860..9964b81f9083 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -495,6 +495,29 @@ export namespace Permission { export type Requested = typeof Requested.Type } +export namespace Command { + const CommandBase = { + ...Base, + command: Schema.String, + } + + /** + * Live waterfall fired before a slash command runs. Listeners return + * `{ parts }` to rewrite the prompt payload, or throw to veto. This is + * the V2 replacement for the legacy V1 `command.execute.before` hook + * and gives plugins proper deny/transform semantics with `next()`. + */ + export const PreExecute = Event.define({ + type: "session.next.command.pre_execute", + schema: { + ...CommandBase, + arguments: Schema.String, + parts: Schema.Array(Schema.Unknown), + }, + }) + export type PreExecute = typeof PreExecute.Type +} + export const Retried = Event.define({ type: "session.next.retried", ...options,