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

This commit is contained in:
Aiden Cline
2025-11-01 01:14:09 -05:00
committed by GitHub
parent c72f8b17c6
commit 041353f4ff
6 changed files with 92 additions and 49 deletions

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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

View File

@@ -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)
}
})
}

View File

@@ -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)
},
)
}

View File

@@ -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: {