Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 151 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1870,7 +1870,157 @@ NOTE: At any point in time through this workflow you should feel free to ask the
export type CommandInput = z.infer<typeof CommandInput>

export async function command(input: CommandInput) {
return runPromise((svc) => svc.command(CommandInput.parse(input)))
log.info("command", input)
const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())

const raw = input.arguments.match(argsRegex) ?? []
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))

const templateCommand = await command.template

const placeholders = templateCommand.match(placeholderRegex) ?? []
let last = 0
for (const item of placeholders) {
const value = Number(item.slice(1))
if (value > last) last = value
}

// Let the final placeholder swallow any extra arguments so prompts read naturally
const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
const position = Number(index)
const argIndex = position - 1
if (argIndex >= args.length) return ""
if (position === last) return args.slice(argIndex).join(" ")
return args[argIndex]
})
const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS")
let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)

// If command doesn't explicitly handle arguments (no $N or $ARGUMENTS placeholders)
// but user provided arguments, append them to the template
if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) {
template = template + "\n\n" + input.arguments
}

const shell = ConfigMarkdown.shell(template)
if (shell.length > 0) {
const results = await Promise.all(
shell.map(async ([, cmd]) => {
try {
return await $`${{ raw: cmd }}`.quiet().nothrow().text()
} catch (error) {
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
}
}),
)
let index = 0
template = template.replace(bashRegex, () => results[index++])
}
template = template.trim()

const agent = await Agent.get(agentName)
const isSubtask = (agent?.mode === "subagent" && command.subtask !== false) || command.subtask === true

const taskModel = await (async () => {
if (isSubtask) {
if (command.model) return Provider.parseModel(command.model)
if (command.agent) {
const m = command.agent === agentName
? agent?.model
: (await Agent.get(command.agent))?.model
if (m) return m
}
if (input.model) return Provider.parseModel(input.model)
return await lastModel(input.sessionID)
}
if (input.model) return Provider.parseModel(input.model)
if (command.model) return Provider.parseModel(command.model)
if (command.agent) {
const m = command.agent === agentName
? agent?.model
: (await Agent.get(command.agent))?.model
if (m) return m
}
return await lastModel(input.sessionID)
})()

try {
await Provider.getModel(taskModel.providerID, taskModel.modelID)
} catch (e) {
if (Provider.ModelNotFoundError.isInstance(e)) {
const { providerID, modelID, suggestions } = e.data
const hint = suggestions?.length ? ` Did you mean: ${suggestions.join(", ")}?` : ""
Bus.publish(Session.Event.Error, {
sessionID: input.sessionID,
error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(),
})
}
throw e
}
if (!agent) {
const available = await Agent.list().then((agents) => agents.filter((a) => !a.hidden).map((a) => a.name))
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
Bus.publish(Session.Event.Error, {
sessionID: input.sessionID,
error: error.toObject(),
})
throw error
}

const templateParts = await resolvePromptParts(template)
const parts = isSubtask
? [
{
type: "subtask" as const,
agent: agent.name,
description: command.description ?? "",
command: input.command,
model: {
providerID: taskModel.providerID,
modelID: taskModel.modelID,
},
// TODO: how can we make task tool accept a more complex input?
prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
},
]
: [...templateParts, ...(input.parts ?? [])]

const userAgent = isSubtask ? (input.agent ?? (await Agent.defaultAgent())) : agentName
const userModel = isSubtask
? input.model
? Provider.parseModel(input.model)
: await lastModel(input.sessionID)
: taskModel

await Plugin.trigger(
"command.execute.before",
{
command: input.command,
sessionID: input.sessionID,
arguments: input.arguments,
},
{ parts },
)

const result = (await prompt({
sessionID: input.sessionID,
messageID: input.messageID,
model: userModel,
agent: userAgent,
parts,
variant: input.variant,
})) as MessageV2.WithParts

Bus.publish(Command.Event.Executed, {
name: input.command,
sessionID: input.sessionID,
arguments: input.arguments,
messageID: result.info.id,
})

return result
}

/** @internal Exported for testing */
Expand Down
Loading