Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,14 @@ export function Autocomplete(props: {
const commands = createMemo((): AutocompleteOption[] => {
const results: AutocompleteOption[] = []
const s = session()

for (const command of sync.data.command) {
if (command.sessionOnly && !s) continue

results.push({
display: "/" + command.name,
description: command.description,
aliases: command.aliases?.map((a) => "/" + a),
onSelect: () => {
const newText = "/" + command.name + " "
const cursor = props.input().logicalCursor
Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,14 +513,15 @@ export function Prompt(props: PromptProps) {
inputText.startsWith("/") &&
iife(() => {
const command = inputText.split(" ")[0].slice(1)
console.log(command)
return sync.data.command.some((x) => x.name === command)
return sync.data.command.some((x) => x.name === command || x.aliases?.includes(command))
})
) {
let [command, ...args] = inputText.split(" ")
const commandName = command.slice(1)
const resolved = sync.data.command.find((x) => x.name === commandName || x.aliases?.includes(commandName))
sdk.client.session.command({
sessionID,
command: command.slice(1),
command: resolved?.name ?? commandName,
arguments: args.join(" "),
agent: local.agent.current().name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
Expand Down
32 changes: 31 additions & 1 deletion packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt"
import { Plugin } from "../plugin"

export namespace Command {
export const Event = {
Expand All @@ -27,7 +28,10 @@ export namespace Command {
agent: z.string().optional(),
model: z.string().optional(),
template: z.string(),
type: z.enum(["template", "plugin"]),
subtask: z.boolean().optional(),
sessionOnly: z.boolean().optional(),
aliases: z.array(z.string()).optional(),
})
.meta({
ref: "Command",
Expand All @@ -45,11 +49,13 @@ export namespace Command {
const result: Record<string, Info> = {
[Default.INIT]: {
name: Default.INIT,
type: "template",
description: "create/update AGENTS.md",
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
},
[Default.REVIEW]: {
name: Default.REVIEW,
type: "template",
description: "review changes [commit|branch|pr], defaults to uncommitted",
template: PROMPT_REVIEW.replace("${path}", Instance.worktree),
subtask: true,
Expand All @@ -59,6 +65,7 @@ export namespace Command {
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = {
name,
type: "template",
agent: command.agent,
model: command.model,
description: command.description,
Expand All @@ -67,11 +74,34 @@ export namespace Command {
}
}

const plugins = await Plugin.list()
for (const plugin of plugins) {
const commands = plugin["plugin.command"]
if (!commands) continue
for (const [name, cmd] of Object.entries(commands)) {
if (result[name]) continue
result[name] = {
name,
type: "plugin",
description: cmd.description,
template: "",
sessionOnly: cmd.sessionOnly,
aliases: cmd.aliases,
}
}
}

return result
})

export async function get(name: string) {
return state().then((x) => x[name])
const commands = await state()
if (commands[name]) return commands[name]
// Check aliases
for (const cmd of Object.values(commands)) {
if (cmd.aliases?.includes(name)) return cmd
}
return undefined
}

export async function list() {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export namespace Config {
return result
}

const PLUGIN_GLOB = new Bun.Glob("plugin/*.{ts,js}")
const PLUGIN_GLOB = new Bun.Glob("{plugin,command}/*.{ts,js}")
async function loadPlugin(dir: string) {
const plugins: string[] = []

Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export namespace Plugin {
}
const mod = await import(plugin)
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
if (typeof fn !== "function") continue
const init = await fn(input)
hooks.push(init)
}
Expand All @@ -53,7 +54,7 @@ export namespace Plugin {
})

export async function trigger<
Name extends Exclude<keyof Required<Hooks>, "auth" | "event" | "tool">,
Name extends Exclude<keyof Required<Hooks>, "auth" | "event" | "tool" | "plugin.command">,
Input = Parameters<Required<Hooks>[Name]>[0],
Output = Parameters<Required<Hooks>[Name]>[1],
>(name: Name, input: Input, output: Output): Promise<Output> {
Expand All @@ -73,6 +74,10 @@ export namespace Plugin {
return state().then((x) => x.hooks)
}

export async function client() {
return state().then((x) => x.input.client)
}

export async function init() {
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
Expand Down
40 changes: 40 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { iife } from "@/util/iife"
import { Shell } from "@/shell/shell"
import { TuiEvent } from "@/cli/cmd/tui/event"

// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
Expand Down Expand Up @@ -1282,6 +1283,45 @@ export namespace SessionPrompt {
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
if (!command) return

// Plugin commands execute directly
if (command.type === "plugin") {
const plugins = await Plugin.list()
for (const plugin of plugins) {
const pluginCommands = plugin["plugin.command"]
const pluginCommand = pluginCommands?.[command.name]
if (!pluginCommand) continue

const client = await Plugin.client()
try {
await pluginCommand.execute({
sessionID: input.sessionID,
arguments: input.arguments,
client,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
log.error("plugin command failed", {
command: command.name,
error: errorMessage,
})
await Bus.publish(TuiEvent.ToastShow, {
title: `/${command.name} failed`,
message: errorMessage,
variant: "error",
})
throw error
}
const last = await Session.messages({
sessionID: input.sessionID,
limit: 1,
})
return last.at(0)
}
return
}

const agentName = command.agent ?? input.agent ?? "build"

const raw = input.arguments.match(argsRegex) ?? []
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,19 @@ export interface Hooks {
input: { sessionID: string; messageID: string; partID: string },
output: { text: string },
) => Promise<void>
/**
* Register custom plugin commands (accessible via /command in TUI)
*/
"plugin.command"?: {
[key: string]: {
description: string
aliases?: string[]
sessionOnly?: boolean
execute(input: {
sessionID?: string
arguments: string
client: ReturnType<typeof createOpencodeClient>
}): Promise<void>
}
}
}
2 changes: 2 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,8 @@ export type Command = {
model?: string
template: string
subtask?: boolean
sessionOnly?: boolean
aliases?: Array<string>
}

export type Model = {
Expand Down
93 changes: 48 additions & 45 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,48 @@ export type EventCommandExecuted = {
}
}

export type EventTuiPromptAppend = {
type: "tui.prompt.append"
properties: {
text: string
}
}

export type EventTuiCommandExecute = {
type: "tui.command.execute"
properties: {
command:
| "session.list"
| "session.new"
| "session.share"
| "session.interrupt"
| "session.compact"
| "session.page.up"
| "session.page.down"
| "session.half.page.up"
| "session.half.page.down"
| "session.first"
| "session.last"
| "prompt.clear"
| "prompt.submit"
| "agent.cycle"
| string
}
}

export type EventTuiToastShow = {
type: "tui.toast.show"
properties: {
title?: string
message: string
variant: "info" | "success" | "warning" | "error"
/**
* Duration in milliseconds
*/
duration?: number
}
}

export type Session = {
id: string
projectID: string
Expand Down Expand Up @@ -639,48 +681,6 @@ export type EventVcsBranchUpdated = {
}
}

export type EventTuiPromptAppend = {
type: "tui.prompt.append"
properties: {
text: string
}
}

export type EventTuiCommandExecute = {
type: "tui.command.execute"
properties: {
command:
| "session.list"
| "session.new"
| "session.share"
| "session.interrupt"
| "session.compact"
| "session.page.up"
| "session.page.down"
| "session.half.page.up"
| "session.half.page.down"
| "session.first"
| "session.last"
| "prompt.clear"
| "prompt.submit"
| "agent.cycle"
| string
}
}

export type EventTuiToastShow = {
type: "tui.toast.show"
properties: {
title?: string
message: string
variant: "info" | "success" | "warning" | "error"
/**
* Duration in milliseconds
*/
duration?: number
}
}

export type Pty = {
id: string
title: string
Expand Down Expand Up @@ -753,16 +753,16 @@ export type Event =
| EventSessionIdle
| EventSessionCompacted
| EventCommandExecuted
| EventTuiPromptAppend
| EventTuiCommandExecute
| EventTuiToastShow
| EventSessionCreated
| EventSessionUpdated
| EventSessionDeleted
| EventSessionDiff
| EventSessionError
| EventFileWatcherUpdated
| EventVcsBranchUpdated
| EventTuiPromptAppend
| EventTuiCommandExecute
| EventTuiToastShow
| EventPtyCreated
| EventPtyUpdated
| EventPtyExited
Expand Down Expand Up @@ -1610,7 +1610,10 @@ export type Command = {
agent?: string
model?: string
template: string
type: "template" | "plugin"
subtask?: boolean
sessionOnly?: boolean
aliases?: Array<string>
}

export type Model = {
Expand Down
Loading
Loading