diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index ae9a74a6..1eae36e6 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -28,8 +28,6 @@ import { Storage } from "@/storage/storage" import { Command } from "@/command" import { Agent as Agents } from "@/agent/agent" import { Permission } from "@/permission" -import { Session } from "@/session" -import { Identifier } from "@/id/id" import { SessionCompaction } from "@/session/compaction" import type { Config } from "@/config/config" import { MCP } from "@/mcp" @@ -89,7 +87,11 @@ export namespace ACP { }) if (!res) return if (res.outcome.outcome !== "selected") { - Permission.respond({ sessionID: permission.sessionID, permissionID: permission.id, response: "reject" }) + Permission.respond({ + sessionID: permission.sessionID, + permissionID: permission.id, + response: "reject", + }) return } Permission.respond({ @@ -111,9 +113,11 @@ export namespace ACP { const acpSession = this.sessionManager.get(part.sessionID) if (!acpSession) return - const message = await Storage.read(["message", part.sessionID, part.messageID]).catch( - () => undefined, - ) + const message = await Storage.read([ + "message", + part.sessionID, + part.messageID, + ]).catch(() => undefined) if (!message || message.role !== "assistant") return if (part.type === "tool") { @@ -192,7 +196,9 @@ export namespace ACP { sessionUpdate: "plan", entries: parsedTodos.data.map((todo) => { const status: PlanEntry["status"] = - todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) + todo.status === "cancelled" + ? "completed" + : (todo.status as PlanEntry["status"]) return { priority: "medium", status, @@ -375,11 +381,6 @@ export namespace ACP { description: command.description ?? "", })) const names = new Set(availableCommands.map((c) => c.name)) - if (!names.has("init")) - availableCommands.push({ - name: "init", - description: "create/update a AGENTS.md", - }) if (!names.has("compact")) availableCommands.push({ name: "compact", @@ -404,7 +405,8 @@ export namespace ACP { description: agent.description, })) - const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id + const currentModeId = + availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id const mcpServers: Record = {} for (const server of params.mcpServers) { @@ -585,14 +587,6 @@ export namespace ACP { } switch (cmd.name) { - case "init": - await Session.initialize({ - sessionID, - messageID: Identifier.ascending("message"), - providerID: model.providerID, - modelID: model.modelID, - }) - break case "compact": await SessionCompaction.run({ sessionID, @@ -665,7 +659,9 @@ export namespace ACP { function parseUri( uri: string, - ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { + ): + | { type: "file"; url: string; filename: string; mime: string } + | { type: "text"; text: string } { try { if (uri.startsWith("file://")) { const path = uri.slice(7) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index c6b24c75..5e1ad9dc 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,8 +1,27 @@ import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +import PROMPT_INITIALIZE from "./template/initialize.txt" +import { Bus } from "../bus" +import { Identifier } from "../id/id" export namespace Command { + export const Default = { + INIT: "init", + } as const + + export const Event = { + Executed: Bus.event( + "command.executed", + z.object({ + name: z.string(), + sessionID: Identifier.schema("session"), + arguments: z.string(), + messageID: Identifier.schema("message"), + }), + ), + } + export const Info = z .object({ name: z.string(), @@ -33,6 +52,14 @@ export namespace Command { } } + if (result[Default.INIT] === undefined) { + result[Default.INIT] = { + name: Default.INIT, + description: "create/update AGENTS.md", + template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree), + } + } + return result }) diff --git a/packages/opencode/src/session/prompt/initialize.txt b/packages/opencode/src/command/template/initialize.txt similarity index 98% rename from packages/opencode/src/session/prompt/initialize.txt rename to packages/opencode/src/command/template/initialize.txt index 4e45b4c7..5bb59c36 100644 --- a/packages/opencode/src/session/prompt/initialize.txt +++ b/packages/opencode/src/command/template/initialize.txt @@ -6,3 +6,5 @@ The file you create will be given to agentic coding agents (such as yourself) th If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them. If there's already an AGENTS.md, improve it if it's located in ${path} + +$ARGUMENTS diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 45e85fd2..27451d22 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -5,6 +5,10 @@ import { LSP } from "../lsp" import { FileWatcher } from "../file/watcher" import { File } from "../file" import { Flag } from "../flag/flag" +import { Project } from "./project" +import { Bus } from "../bus" +import { Command } from "../command" +import { Instance } from "./instance" export async function InstanceBootstrap() { if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return @@ -14,4 +18,10 @@ export async function InstanceBootstrap() { await LSP.init() FileWatcher.init() File.init() + + Bus.subscribe(Command.Event.Executed, async (payload) => { + if (payload.properties.name === Command.Default.INIT) { + await Project.setInitialized(Instance.project.id) + } + }) } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 2cd1bcd2..71d59d84 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -2,8 +2,6 @@ import { Decimal } from "decimal.js" import z from "zod" import { type LanguageModelUsage, type ProviderMetadata } from "ai" -import PROMPT_INITIALIZE from "../session/prompt/initialize.txt" - import { Bus } from "../bus" import { Config } from "../config/config" import { Flag } from "../flag/flag" @@ -14,11 +12,11 @@ import { Share } from "../share/share" import { Storage } from "../storage/storage" import { Log } from "../util/log" import { MessageV2 } from "./message-v2" -import { Project } from "../project/project" import { Instance } from "../project/instance" import { SessionPrompt } from "./prompt" import { fn } from "@/util/fn" import { Snapshot } from "@/snapshot" +import { Command } from "../command" export namespace Session { const log = Log.create({ service: "session" }) @@ -164,7 +162,12 @@ export namespace Session { }) }) - export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { + export async function createNext(input: { + id?: string + title?: string + parentID?: string + directory: string + }) { const result: Info = { id: Identifier.descending("session", input.id), version: Installation.VERSION, @@ -402,7 +405,9 @@ export namespace Session { .add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000)) .add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000)) .add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000)) + .add( + new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000), + ) .toNumber(), tokens, } @@ -423,22 +428,13 @@ export namespace Session { messageID: Identifier.schema("message"), }), async (input) => { - await SessionPrompt.prompt({ + await SessionPrompt.command({ sessionID: input.sessionID, messageID: input.messageID, - model: { - providerID: input.providerID, - modelID: input.modelID, - }, - parts: [ - { - id: Identifier.ascending("part"), - type: "text", - text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree), - }, - ], + model: input.providerID + "/" + input.modelID, + command: Command.Default.INIT, + arguments: "", }) - await Project.setInitialized(Instance.project.id) }, ) } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2f150586..5cbfb8b5 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1593,6 +1593,7 @@ export namespace SessionPrompt { let index = 0 template = template.replace(bashRegex, () => results[index++]) } + template = template.trim() const parts = [ { @@ -1657,6 +1658,8 @@ export namespace SessionPrompt { })() const agent = await Agent.get(agentName) + let result: MessageV2.WithParts + if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) { using abort = lock(input.sessionID) @@ -1732,7 +1735,7 @@ export namespace SessionPrompt { } await Session.updatePart(toolPart) - const result = await TaskTool.init().then((t) => + const taskResult = await TaskTool.init().then((t) => t.execute(args, { sessionID: input.sessionID, abort: abort.signal, @@ -1760,22 +1763,31 @@ export namespace SessionPrompt { }, input: toolPart.state.input, title: "", - metadata: result.metadata, - output: result.output, + metadata: taskResult.metadata, + output: taskResult.output, } await Session.updatePart(toolPart) } - return { info: assistantMsg, parts: [toolPart] } + result = { info: assistantMsg, parts: [toolPart] } + } else { + result = await prompt({ + sessionID: input.sessionID, + messageID: input.messageID, + model, + agent: agentName, + parts, + }) } - return prompt({ + Bus.publish(Command.Event.Executed, { + name: input.command, sessionID: input.sessionID, - messageID: input.messageID, - model, - agent: agentName, - parts, + arguments: input.arguments, + messageID: result.info.id, }) + + return result } async function ensureTitle(input: {