Skip to content

Commit afafc52

Browse files
refactor
1 parent 929af33 commit afafc52

File tree

8 files changed

+65
-125
lines changed

8 files changed

+65
-125
lines changed

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -187,26 +187,9 @@ export function Autocomplete(props: {
187187
const results: AutocompleteOption[] = []
188188
const s = session()
189189

190-
// Plugin commands
191-
for (const pluginCmd of sync.data.pluginCommand) {
192-
if (pluginCmd.sessionOnly && !s) continue
193-
results.push({
194-
display: "/" + pluginCmd.name,
195-
aliases: pluginCmd.aliases?.map((a) => "/" + a),
196-
description: pluginCmd.description,
197-
onSelect: async () => {
198-
await sdk.client.pluginCommand.execute({
199-
path: { name: pluginCmd.name },
200-
body: { sessionID: props.sessionID },
201-
})
202-
// Clear the input
203-
const cursor = props.input().logicalCursor
204-
props.input().deleteRange(0, 0, cursor.row, cursor.col)
205-
},
206-
})
207-
}
208-
209190
for (const command of sync.data.command) {
191+
if (command.sessionOnly && !s) continue
192+
210193
results.push({
211194
display: "/" + command.name,
212195
description: command.description,

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
2828
provider: Provider[]
2929
agent: Agent[]
3030
command: Command[]
31-
pluginCommand: Array<{
32-
name: string
33-
description: string
34-
aliases?: string[]
35-
sessionOnly?: boolean
36-
}>
3731
permission: {
3832
[sessionID: string]: Permission[]
3933
}
@@ -62,8 +56,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
6256
agent: [],
6357
permission: {},
6458
command: [],
65-
pluginCommand: [],
6659
provider: [],
60+
6761
session: [],
6862
session_diff: {},
6963
todo: {},
@@ -244,8 +238,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
244238
),
245239
),
246240
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
247-
sdk.client.pluginCommand.list().then((x) => setStore("pluginCommand", x.data ?? [])),
248241
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
242+
249243
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
250244
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
251245
]).then(() => {

packages/opencode/src/command/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Instance } from "../project/instance"
44
import PROMPT_INITIALIZE from "./template/initialize.txt"
55
import { Bus } from "../bus"
66
import { Identifier } from "../id/id"
7+
import { Plugin } from "../plugin"
78

89
export namespace Command {
910
export const Default = {
@@ -30,6 +31,7 @@ export namespace Command {
3031
model: z.string().optional(),
3132
template: z.string(),
3233
subtask: z.boolean().optional(),
34+
sessionOnly: z.boolean().optional(),
3335
})
3436
.meta({
3537
ref: "Command",
@@ -60,6 +62,21 @@ export namespace Command {
6062
}
6163
}
6264

65+
const plugins = await Plugin.list()
66+
for (const plugin of plugins) {
67+
const commands = plugin["plugin.command"]
68+
if (!commands) continue
69+
for (const [name, cmd] of Object.entries(commands)) {
70+
if (result[name]) continue
71+
result[name] = {
72+
name,
73+
description: cmd.description,
74+
template: "",
75+
sessionOnly: cmd.sessionOnly,
76+
}
77+
}
78+
}
79+
6380
return result
6481
})
6582

packages/opencode/src/plugin/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export namespace Plugin {
7373
return state().then((x) => x.hooks)
7474
}
7575

76+
export async function client() {
77+
return state().then((x) => x.input.client)
78+
}
79+
7680
export async function init() {
7781
const hooks = await state().then((x) => x.hooks)
7882
const config = await Config.get()

packages/opencode/src/server/server.ts

Lines changed: 1 addition & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,103 +1108,9 @@ export namespace Server {
11081108
return c.json(commands)
11091109
},
11101110
)
1111-
.get(
1112-
"/plugin/command",
1113-
describeRoute({
1114-
description: "List all plugin commands",
1115-
operationId: "pluginCommand.list",
1116-
responses: {
1117-
200: {
1118-
description: "List of plugin commands",
1119-
content: {
1120-
"application/json": {
1121-
schema: resolver(
1122-
z
1123-
.array(
1124-
z.object({
1125-
name: z.string(),
1126-
description: z.string(),
1127-
aliases: z.array(z.string()).optional(),
1128-
sessionOnly: z.boolean().optional(),
1129-
}),
1130-
)
1131-
.meta({ ref: "PluginCommand" }),
1132-
),
1133-
},
1134-
},
1135-
},
1136-
},
1137-
}),
1138-
async (c) => {
1139-
const plugins = await Plugin.list()
1140-
const commands = []
1141-
for (const plugin of plugins) {
1142-
const pluginCommands = plugin["plugin.command"]
1143-
if (!pluginCommands) continue
1144-
for (const [name, def] of Object.entries(pluginCommands)) {
1145-
commands.push({
1146-
name,
1147-
description: def.description,
1148-
aliases: def.aliases,
1149-
sessionOnly: def.sessionOnly,
1150-
})
1151-
}
1152-
}
1153-
return c.json(commands)
1154-
},
1155-
)
1156-
.post(
1157-
"/plugin/command/:name/execute",
1158-
describeRoute({
1159-
description: "Execute a plugin command",
1160-
operationId: "pluginCommand.execute",
1161-
responses: {
1162-
200: {
1163-
description: "Command executed",
1164-
content: {
1165-
"application/json": {
1166-
schema: resolver(z.boolean()),
1167-
},
1168-
},
1169-
},
1170-
...errors(400, 404),
1171-
},
1172-
}),
1173-
validator(
1174-
"param",
1175-
z.object({
1176-
name: z.string().meta({ description: "Plugin command name" }),
1177-
}),
1178-
),
1179-
validator(
1180-
"json",
1181-
z.object({
1182-
sessionID: z.string().optional().meta({ description: "Session ID" }),
1183-
}),
1184-
),
1185-
async (c) => {
1186-
const { name } = c.req.valid("param")
1187-
const { sessionID } = c.req.valid("json")
1188-
1189-
const plugins = await Plugin.list()
1190-
for (const plugin of plugins) {
1191-
const command = plugin["plugin.command"]?.[name]
1192-
if (!command) continue
1193-
1194-
const client = createOpencodeClient({
1195-
baseUrl: "http://localhost:4096",
1196-
fetch: async (...args) => Server.App().fetch(...args),
1197-
})
1198-
1199-
await command.execute({ sessionID, client })
1200-
return c.json(true)
1201-
}
1202-
1203-
return c.json({ error: "Command not found" }, 404)
1204-
},
1205-
)
12061111
.get(
12071112
"/config/providers",
1113+
12081114
describeRoute({
12091115
description: "List all providers",
12101116
operationId: "config.providers",

packages/opencode/src/session/prompt.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1631,7 +1631,42 @@ export namespace SessionPrompt {
16311631
export async function command(input: CommandInput) {
16321632
log.info("command", input)
16331633
const command = await Command.get(input.command)
1634-
const agentName = command.agent ?? input.agent ?? "build"
1634+
const agentName = command?.agent ?? input.agent ?? "build"
1635+
1636+
const plugins = await Plugin.list()
1637+
for (const plugin of plugins) {
1638+
const pluginCommands = plugin["plugin.command"]
1639+
const pluginCommand = pluginCommands?.[input.command]
1640+
if (!pluginCommand) continue
1641+
1642+
const client = await Plugin.client()
1643+
await pluginCommand.execute({ sessionID: input.sessionID, client })
1644+
const last = await Session.messages({ sessionID: input.sessionID, limit: 1 })
1645+
const message = last.at(0)
1646+
if (message) return message
1647+
return await SessionPrompt.prompt({
1648+
sessionID: input.sessionID,
1649+
agent: agentName,
1650+
parts: [
1651+
{
1652+
type: "text",
1653+
text: "",
1654+
},
1655+
],
1656+
})
1657+
}
1658+
1659+
if (!command)
1660+
return await SessionPrompt.prompt({
1661+
sessionID: input.sessionID,
1662+
agent: agentName,
1663+
parts: [
1664+
{
1665+
type: "text",
1666+
text: "",
1667+
},
1668+
],
1669+
})
16351670

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

packages/sdk/js/src/gen/types.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,7 @@ export type Command = {
12151215
model?: string
12161216
template: string
12171217
subtask?: boolean
1218+
sessionOnly?: boolean
12181219
}
12191220

12201221
export type Model = {

packages/web/src/content/docs/plugins.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ export const CommandsPlugin: Plugin = async (ctx) => {
166166
}
167167
```
168168

169-
The `plugin.command` hook lets you define commands that appear in TUI autocomplete. Each command has:
169+
The `plugin.command` hook lets you define commands that appear in command listings. Each command has:
170170

171171
- `description`: Brief description shown in autocomplete
172172
- `aliases`: Alternative names for the command (optional)
173173
- `sessionOnly`: Whether command requires an active session (optional)
174174
- `execute`: Function that runs when the command is invoked
175175

176-
Commands are invoked by typing `/command-name` in the TUI.
176+
Commands are invoked by typing `/command-name` in the TUI.

0 commit comments

Comments
 (0)