Przeglądaj źródła

make /init a default slash command on server side (#3677)

Aiden Cline 3 miesięcy temu
rodzic
commit
041353f4ff

+ 18 - 22
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<MessageV2.Info>(["message", part.sessionID, part.messageID]).catch(
-          () => undefined,
-        )
+        const message = await Storage.read<MessageV2.Info>([
+          "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<string, Config.Mcp> = {}
       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)

+ 27 - 0
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
   })
 

+ 2 - 0
packages/opencode/src/session/prompt/initialize.txt → 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

+ 10 - 0
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)
+    }
+  })
 }

+ 14 - 18
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)
     },
   )
 }

+ 21 - 9
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: {