diff --git a/packages/core/src/plugin/plan.ts b/packages/core/src/plugin/plan.ts index a2bd14537cc..46dfc9bec4c 100644 --- a/packages/core/src/plugin/plan.ts +++ b/packages/core/src/plugin/plan.ts @@ -2,14 +2,24 @@ export * as PlanPlugin from "./plan" import { ToolFailure } from "@opencode-ai/ai" import { define } from "@opencode-ai/plugin/effect/plugin" -import { Effect } from "effect" +import { Effect, Stream } from "effect" import { Agent } from "../agent" +const plan = Agent.ID.make("plan") + +const enter = ` +You are in Plan mode. This is a read-only mode. Do not modify files or take other write actions. Do not delegate work to a subagent that can make edits. Investigate, ask questions, and propose a plan. +` + +const leave = ` +You are no longer in Plan mode. The previous read-only restrictions no longer apply. You may edit files and use write tools again. +` + export const Plugin = define({ id: "opencode.plan", effect: Effect.fn(function* (ctx) { yield* ctx.agent.transform((draft) => { - draft.update(Agent.ID.make("plan"), (item) => { + draft.update(plan, (item) => { item.name = Agent.Name.make("Plan") item.description = "Read-only agent for exploring the codebase and planning work before implementation." item.mode = "primary" @@ -18,11 +28,28 @@ export const Plugin = define({ }) yield* ctx.tool.hook("execute.before", (event) => { - if (event.agent !== Agent.ID.make("plan")) return Effect.void + if (event.agent !== plan) return Effect.void if (event.tool !== "edit" && event.tool !== "write" && event.tool !== "patch") return Effect.void return new ToolFailure({ message: `Cannot use ${event.tool} in Plan mode. You are in a read-only mode and must not modify files.`, }) }) + + yield* ctx.event.subscribe().pipe( + Stream.filter((event) => event.type === "session.agent.selected"), + Stream.runForEach((event) => { + if (event.data.agent === event.data.previous) return Effect.void + const text = event.data.agent === plan ? enter : event.data.previous === plan ? leave : undefined + if (!text) return Effect.void + return ctx.session + .synthetic({ + sessionID: event.data.sessionID, + text, + resume: false, + }) + .pipe(Effect.catch(() => Effect.void)) + }), + Effect.forkScoped({ startImmediately: true }), + ) }), })