summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax <[email protected]>2025-08-22 17:04:28 -0400
committerGitHub <[email protected]>2025-08-22 17:04:28 -0400
commit133fe41cd5fb0ec3dc03a90e58526408297878fd (patch)
tree82aac357a61fe9e7a6802974f6c7cf169a9f22a8 /packages
parent74c1085103e130cb07522e3ad725f17eca3e5832 (diff)
downloadopencode-133fe41cd5fb0ec3dc03a90e58526408297878fd.tar.gz
opencode-133fe41cd5fb0ec3dc03a90e58526408297878fd.zip
slash commands (#2157)
Co-authored-by: adamdotdevin <[email protected]>
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/opencode/script/schema.ts1
-rw-r--r--packages/opencode/src/command/index.ts44
-rw-r--r--packages/opencode/src/config/config.ts35
-rw-r--r--packages/opencode/src/provider/provider.ts4
-rw-r--r--packages/opencode/src/server/server.ts65
-rw-r--r--packages/opencode/src/session/index.ts74
-rw-r--r--packages/sdk/go/.stats.yml8
-rw-r--r--packages/sdk/go/api.md12
-rw-r--r--packages/sdk/go/client.go2
-rw-r--r--packages/sdk/go/command.go67
-rw-r--r--packages/sdk/go/command_test.go36
-rw-r--r--packages/sdk/go/config.go30
-rw-r--r--packages/sdk/go/session.go47
-rw-r--r--packages/sdk/go/session_test.go32
-rw-r--r--packages/sdk/js/src/gen/sdk.gen.ts31
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts62
-rw-r--r--packages/sdk/stainless/stainless.yml7
-rw-r--r--packages/tui/internal/app/app.go43
-rw-r--r--packages/tui/internal/commands/command.go14
-rw-r--r--packages/tui/internal/components/chat/editor.go28
-rw-r--r--packages/tui/internal/components/chat/messages.go4
-rw-r--r--packages/tui/internal/tui/tui.go18
-rw-r--r--packages/web/astro.config.mjs9
-rw-r--r--packages/web/src/content/docs/docs/commands.mdx167
-rw-r--r--packages/web/src/content/docs/docs/index.mdx26
25 files changed, 821 insertions, 45 deletions
diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts
index 008c168cb..c815c7324 100755
--- a/packages/opencode/script/schema.ts
+++ b/packages/opencode/script/schema.ts
@@ -5,6 +5,7 @@ import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const file = process.argv[2]
+console.log(file)
const result = zodToJsonSchema(Config.Info, {
/**
diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts
new file mode 100644
index 000000000..97dd36a05
--- /dev/null
+++ b/packages/opencode/src/command/index.ts
@@ -0,0 +1,44 @@
+import z from "zod"
+import { App } from "../app/app"
+import { Config } from "../config/config"
+
+export namespace Command {
+ export const Info = z
+ .object({
+ name: z.string(),
+ description: z.string().optional(),
+ agent: z.string().optional(),
+ model: z.string().optional(),
+ template: z.string(),
+ })
+ .openapi({
+ ref: "Command",
+ })
+ export type Info = z.infer<typeof Info>
+
+ const state = App.state("command", async () => {
+ const cfg = await Config.get()
+
+ const result: Record<string, Info> = {}
+
+ for (const [name, command] of Object.entries(cfg.command ?? {})) {
+ result[name] = {
+ name,
+ agent: command.agent,
+ model: command.model,
+ description: command.description,
+ template: command.template,
+ }
+ }
+
+ return result
+ })
+
+ export async function get(name: string) {
+ return state().then((x) => x[name])
+ }
+
+ export async function list() {
+ return state().then((x) => Object.values(x))
+ }
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 13a009ad9..f707d35ff 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -107,6 +107,32 @@ export namespace Config {
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
+
+ // Load command markdown files
+ result.command = result.command || {}
+ const markdownCommands = [
+ ...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)),
+ ...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)),
+ ]
+ for (const item of markdownCommands) {
+ const content = await Bun.file(item).text()
+ const md = matter(content)
+ if (!md.data) continue
+
+ const config = {
+ name: path.basename(item, ".md"),
+ ...md.data,
+ template: md.content.trim(),
+ }
+ const parsed = Command.safeParse(config)
+ if (parsed.success) {
+ result.command = mergeDeep(result.command, {
+ [config.name]: parsed.data,
+ })
+ continue
+ }
+ throw new InvalidError({ path: item }, { cause: parsed.error })
+ }
// Migrate deprecated mode field to agent field
for (const [name, mode] of Object.entries(result.mode)) {
result.agent = mergeDeep(result.agent ?? {}, {
@@ -192,6 +218,14 @@ export namespace Config {
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
export type Permission = z.infer<typeof Permission>
+ export const Command = z.object({
+ template: z.string(),
+ description: z.string().optional(),
+ agent: z.string().optional(),
+ model: z.string().optional(),
+ })
+ export type Command = z.infer<typeof Command>
+
export const Agent = z
.object({
model: z.string().optional(),
@@ -305,6 +339,7 @@ export namespace Config {
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
tui: TUI.optional().describe("TUI specific settings"),
+ command: z.record(z.string(), Command).optional(),
plugin: z.string().array().optional(),
snapshot: z.boolean().optional(),
share: z
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 2fe22c77b..e37f11f06 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -36,9 +36,9 @@ export namespace Provider {
},
}
},
- async opencode() {
+ async opencode(input) {
return {
- autoload: true,
+ autoload: Object.keys(input.models).length > 0,
options: {},
}
},
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 69805fbee..31951eede 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -21,6 +21,7 @@ import { Permission } from "../permission"
import { lazy } from "../util/lazy"
import { Agent } from "../agent/agent"
import { Auth } from "../auth"
+import { Command } from "../command"
const ERRORS = {
400: {
@@ -611,10 +612,12 @@ export namespace Server {
description: "Created message",
content: {
"application/json": {
- schema: resolver(z.object({
+ schema: resolver(
+ z.object({
info: MessageV2.Assistant,
parts: MessageV2.Part.array(),
- })),
+ }),
+ ),
},
},
},
@@ -635,6 +638,41 @@ export namespace Server {
},
)
.post(
+ "/session/:id/command",
+ describeRoute({
+ description: "Send a new command to a session",
+ operationId: "session.command",
+ responses: {
+ 200: {
+ description: "Created message",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z.object({
+ info: MessageV2.Assistant,
+ parts: MessageV2.Part.array(),
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "param",
+ z.object({
+ id: z.string().openapi({ description: "Session ID" }),
+ }),
+ ),
+ zValidator("json", Session.CommandInput.omit({ sessionID: true })),
+ async (c) => {
+ const sessionID = c.req.valid("param").id
+ const body = c.req.valid("json")
+ const msg = await Session.command({ ...body, sessionID })
+ return c.json(msg)
+ },
+ )
+ .post(
"/session/:id/shell",
describeRoute({
description: "Run a shell command",
@@ -656,7 +694,7 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
- zValidator("json", Session.CommandInput.omit({ sessionID: true })),
+ zValidator("json", Session.ShellInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
@@ -754,6 +792,27 @@ export namespace Server {
},
)
.get(
+ "/command",
+ describeRoute({
+ description: "List all commands",
+ operationId: "command.list",
+ responses: {
+ 200: {
+ description: "List of commands",
+ content: {
+ "application/json": {
+ schema: resolver(Command.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const commands = await Command.list()
+ return c.json(commands)
+ },
+ )
+ .get(
"/config/providers",
describeRoute({
description: "List all providers",
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 24cffdef1..37362f34b 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -47,6 +47,8 @@ import { Permission } from "../permission"
import { Wildcard } from "../util/wildcard"
import { ulid } from "ulid"
import { defer } from "../util/defer"
+import { Command } from "../command"
+import { $ } from "bun"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -1025,13 +1027,13 @@ export namespace Session {
return result
}
- export const CommandInput = z.object({
+ export const ShellInput = z.object({
sessionID: Identifier.schema("session"),
agent: z.string(),
command: z.string(),
})
- export type CommandInput = z.infer<typeof CommandInput>
- export async function shell(input: CommandInput) {
+ export type ShellInput = z.infer<typeof ShellInput>
+ export async function shell(input: ShellInput) {
using abort = lock(input.sessionID)
const msg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
@@ -1155,6 +1157,72 @@ export namespace Session {
return { info: msg, parts: [part] }
}
+ export const CommandInput = z.object({
+ messageID: Identifier.schema("message").optional(),
+ sessionID: Identifier.schema("session"),
+ agent: z.string().optional(),
+ model: z.string().optional(),
+ arguments: z.string(),
+ command: z.string(),
+ })
+ export type CommandInput = z.infer<typeof CommandInput>
+ const bashRegex = /!`([^`]+)`/g
+ const fileRegex = /@([^\s]+)/g
+
+ export async function command(input: CommandInput) {
+ const command = await Command.get(input.command)
+ const agent = input.agent ?? command.agent ?? "build"
+ const model =
+ input.model ??
+ command.model ??
+ (await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ??
+ (await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`))
+ let template = command.template.replace("$ARGUMENTS", input.arguments)
+
+ const bash = Array.from(template.matchAll(bashRegex))
+ if (bash.length > 0) {
+ const results = await Promise.all(
+ bash.map(async ([, cmd]) => {
+ try {
+ return await $`${{ raw: cmd }}`.nothrow().text()
+ } catch (error) {
+ return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
+ }
+ }),
+ )
+ let index = 0
+ template = template.replace(bashRegex, () => results[index++])
+ }
+
+ const parts = [
+ {
+ type: "text",
+ text: template,
+ },
+ ] as ChatInput["parts"]
+
+ const matches = template.matchAll(fileRegex)
+ const app = App.info()
+
+ for (const match of matches) {
+ const file = path.join(app.path.cwd, match[1])
+ parts.push({
+ type: "file",
+ url: `file://${file}`,
+ filename: match[1],
+ mime: "text/plain",
+ })
+ }
+
+ return chat({
+ sessionID: input.sessionID,
+ messageID: input.messageID,
+ ...Provider.parseModel(model!),
+ agent,
+ parts,
+ })
+ }
+
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
const toolcalls: Record<string, MessageV2.ToolPart> = {}
let snapshot: string | undefined
diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml
index 5f222f03f..149b4c62c 100644
--- a/packages/sdk/go/.stats.yml
+++ b/packages/sdk/go/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 39
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-be3e40e0bf7dde2bb15ff82d5d104418fb47fe335808a1aa6468b0be2210a88f.yml
-openapi_spec_hash: c1bbb3ebd807656bd9f31a618077e76b
-config_hash: eab3723c4c2232a6ba1821151259d6da
+configured_endpoints: 41
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d5200eaa145f567a58daa78941ab1141dd63f5f0cfe1596d5c9ecf12d34fea35.yml
+openapi_spec_hash: abeb66291dc158f2cdc90bf9945e283e
+config_hash: fb625e876313a9f8f31532348fa91f59
diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md
index 5accfcb50..2f9eadb6a 100644
--- a/packages/sdk/go/api.md
+++ b/packages/sdk/go/api.md
@@ -70,6 +70,16 @@ Methods:
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+# Command
+
+Response Types:
+
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>
+
+Methods:
+
+- <code title="get /command">client.Command.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
# Session
Params Types:
@@ -106,6 +116,7 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
@@ -118,6 +129,7 @@ Methods:
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
diff --git a/packages/sdk/go/client.go b/packages/sdk/go/client.go
index 6baf21a8f..286408ab6 100644
--- a/packages/sdk/go/client.go
+++ b/packages/sdk/go/client.go
@@ -21,6 +21,7 @@ type Client struct {
Find *FindService
File *FileService
Config *ConfigService
+ Command *CommandService
Session *SessionService
Tui *TuiService
}
@@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) {
r.Find = NewFindService(opts...)
r.File = NewFileService(opts...)
r.Config = NewConfigService(opts...)
+ r.Command = NewCommandService(opts...)
r.Session = NewSessionService(opts...)
r.Tui = NewTuiService(opts...)
diff --git a/packages/sdk/go/command.go b/packages/sdk/go/command.go
new file mode 100644
index 000000000..9ca70c3ac
--- /dev/null
+++ b/packages/sdk/go/command.go
@@ -0,0 +1,67 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+// CommandService contains methods and other services that help with interacting
+// with the opencode API.
+//
+// Note, unlike clients, this service does not read variables from the environment
+// automatically. You should not instantiate this service directly, and instead use
+// the [NewCommandService] method instead.
+type CommandService struct {
+ Options []option.RequestOption
+}
+
+// NewCommandService generates a new service that applies the given options to each
+// request. These options are applied after the parent client's options (if there
+// is one), and before any request-specific options.
+func NewCommandService(opts ...option.RequestOption) (r *CommandService) {
+ r = &CommandService{}
+ r.Options = opts
+ return
+}
+
+// List all commands
+func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "command"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type Command struct {
+ Name string `json:"name,required"`
+ Template string `json:"template,required"`
+ Agent string `json:"agent"`
+ Description string `json:"description"`
+ Model string `json:"model"`
+ JSON commandJSON `json:"-"`
+}
+
+// commandJSON contains the JSON metadata for the struct [Command]
+type commandJSON struct {
+ Name apijson.Field
+ Template apijson.Field
+ Agent apijson.Field
+ Description apijson.Field
+ Model apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Command) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r commandJSON) RawJSON() string {
+ return r.raw
+}
diff --git a/packages/sdk/go/command_test.go b/packages/sdk/go/command_test.go
new file mode 100644
index 000000000..5fd8c37be
--- /dev/null
+++ b/packages/sdk/go/command_test.go
@@ -0,0 +1,36 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode_test
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/sst/opencode-sdk-go"
+ "github.com/sst/opencode-sdk-go/internal/testutil"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+func TestCommandList(t *testing.T) {
+ t.Skip("skipped: tests are disabled for the time being")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := opencode.NewClient(
+ option.WithBaseURL(baseURL),
+ )
+ _, err := client.Command.List(context.TODO())
+ if err != nil {
+ var apierr *opencode.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go
index aae6e5e2b..59db54b95 100644
--- a/packages/sdk/go/config.go
+++ b/packages/sdk/go/config.go
@@ -49,7 +49,8 @@ type Config struct {
// automatically
Autoshare bool `json:"autoshare"`
// Automatically update to the latest version
- Autoupdate bool `json:"autoupdate"`
+ Autoupdate bool `json:"autoupdate"`
+ Command map[string]ConfigCommand `json:"command"`
// Disable providers that are loaded automatically
DisabledProviders []string `json:"disabled_providers"`
Experimental ConfigExperimental `json:"experimental"`
@@ -94,6 +95,7 @@ type configJSON struct {
Agent apijson.Field
Autoshare apijson.Field
Autoupdate apijson.Field
+ Command apijson.Field
DisabledProviders apijson.Field
Experimental apijson.Field
Formatter apijson.Field
@@ -664,6 +666,32 @@ func (r ConfigAgentPlanPermissionWebfetch) IsKnown() bool {
return false
}
+type ConfigCommand struct {
+ Template string `json:"template,required"`
+ Agent string `json:"agent"`
+ Description string `json:"description"`
+ Model string `json:"model"`
+ JSON configCommandJSON `json:"-"`
+}
+
+// configCommandJSON contains the JSON metadata for the struct [ConfigCommand]
+type configCommandJSON struct {
+ Template apijson.Field
+ Agent apijson.Field
+ Description apijson.Field
+ Model apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigCommand) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configCommandJSON) RawJSON() string {
+ return r.raw
+}
+
type ConfigExperimental struct {
Hook ConfigExperimentalHook `json:"hook"`
JSON configExperimentalJSON `json:"-"`
diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go
index a4ad09e25..237b490d7 100644
--- a/packages/sdk/go/session.go
+++ b/packages/sdk/go/session.go
@@ -114,6 +114,18 @@ func (r *SessionService) Children(ctx context.Context, id string, opts ...option
return
}
+// Send a new command to a session
+func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/command", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
// Get session
func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
opts = append(r.Options[:], opts...)
@@ -2301,6 +2313,29 @@ func (r sessionChatResponseJSON) RawJSON() string {
return r.raw
}
+type SessionCommandResponse struct {
+ Info AssistantMessage `json:"info,required"`
+ Parts []Part `json:"parts,required"`
+ JSON sessionCommandResponseJSON `json:"-"`
+}
+
+// sessionCommandResponseJSON contains the JSON metadata for the struct
+// [SessionCommandResponse]
+type sessionCommandResponseJSON struct {
+ Info apijson.Field
+ Parts apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *SessionCommandResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionCommandResponseJSON) RawJSON() string {
+ return r.raw
+}
+
type SessionMessageResponse struct {
Info Message `json:"info,required"`
Parts []Part `json:"parts,required"`
@@ -2419,6 +2454,18 @@ func (r SessionChatParamsPartsType) IsKnown() bool {
return false
}
+type SessionCommandParams struct {
+ Arguments param.Field[string] `json:"arguments,required"`
+ Command param.Field[string] `json:"command,required"`
+ Agent param.Field[string] `json:"agent"`
+ MessageID param.Field[string] `json:"messageID"`
+ Model param.Field[string] `json:"model"`
+}
+
+func (r SessionCommandParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
type SessionInitParams struct {
MessageID param.Field[string] `json:"messageID,required"`
ModelID param.Field[string] `json:"modelID,required"`
diff --git a/packages/sdk/go/session_test.go b/packages/sdk/go/session_test.go
index 58e68dc1c..d67be255a 100644
--- a/packages/sdk/go/session_test.go
+++ b/packages/sdk/go/session_test.go
@@ -199,6 +199,38 @@ func TestSessionChildren(t *testing.T) {
}
}
+func TestSessionCommandWithOptionalParams(t *testing.T) {
+ t.Skip("skipped: tests are disabled for the time being")
+ baseURL := "http://localhost:4010"
+ if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+ baseURL = envURL
+ }
+ if !testutil.CheckTestServer(t, baseURL) {
+ return
+ }
+ client := opencode.NewClient(
+ option.WithBaseURL(baseURL),
+ )
+ _, err := client.Session.Command(
+ context.TODO(),
+ "id",
+ opencode.SessionCommandParams{
+ Arguments: opencode.F("arguments"),
+ Command: opencode.F("command"),
+ Agent: opencode.F("agent"),
+ MessageID: opencode.F("msg"),
+ Model: opencode.F("model"),
+ },
+ )
+ if err != nil {
+ var apierr *opencode.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
+
func TestSessionGet(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts
index b5e055408..b00216b83 100644
--- a/packages/sdk/js/src/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/gen/sdk.gen.ts
@@ -39,6 +39,8 @@ import type {
SessionChatResponses,
SessionMessageData,
SessionMessageResponses,
+ SessionCommandData,
+ SessionCommandResponses,
SessionShellData,
SessionShellResponses,
SessionRevertData,
@@ -47,6 +49,8 @@ import type {
SessionUnrevertResponses,
PostSessionByIdPermissionsByPermissionIdData,
PostSessionByIdPermissionsByPermissionIdResponses,
+ CommandListData,
+ CommandListResponses,
ConfigProvidersData,
ConfigProvidersResponses,
FindTextData,
@@ -356,6 +360,20 @@ class Session extends _HeyApiClient {
}
/**
+ * Send a new command to a session
+ */
+ public command<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
+ return (options.client ?? this._client).post<SessionCommandResponses, unknown, ThrowOnError>({
+ url: "/session/{id}/command",
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ })
+ }
+
+ /**
* Run a shell command
*/
public shell<ThrowOnError extends boolean = false>(options: Options<SessionShellData, ThrowOnError>) {
@@ -394,6 +412,18 @@ class Session extends _HeyApiClient {
}
}
+class Command extends _HeyApiClient {
+ /**
+ * List all commands
+ */
+ public list<ThrowOnError extends boolean = false>(options?: Options<CommandListData, ThrowOnError>) {
+ return (options?.client ?? this._client).get<CommandListResponses, unknown, ThrowOnError>({
+ url: "/command",
+ ...options,
+ })
+ }
+}
+
class Find extends _HeyApiClient {
/**
* Find text in files
@@ -592,6 +622,7 @@ export class OpencodeClient extends _HeyApiClient {
app = new App({ client: this._client })
config = new Config({ client: this._client })
session = new Session({ client: this._client })
+ command = new Command({ client: this._client })
find = new Find({ client: this._client })
file = new File({ client: this._client })
tui = new Tui({ client: this._client })
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 9919dc419..8e9662ad5 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -585,6 +585,14 @@ export type Config = {
*/
scroll_speed: number
}
+ command?: {
+ [key: string]: {
+ template: string
+ description?: string
+ agent?: string
+ model?: string
+ }
+ }
plugin?: Array<string>
snapshot?: boolean
/**
@@ -1110,6 +1118,14 @@ export type AgentPartInput = {
}
}
+export type Command = {
+ name: string
+ description?: string
+ agent?: string
+ model?: string
+ template: string
+}
+
export type Symbol = {
name: string
kind: number
@@ -1563,6 +1579,36 @@ export type SessionMessageResponses = {
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
+export type SessionCommandData = {
+ body?: {
+ messageID?: string
+ agent?: string
+ model?: string
+ arguments: string
+ command: string
+ }
+ path: {
+ /**
+ * Session ID
+ */
+ id: string
+ }
+ query?: never
+ url: "/session/{id}/command"
+}
+
+export type SessionCommandResponses = {
+ /**
+ * Created message
+ */
+ 200: {
+ info: AssistantMessage
+ parts: Array<Part>
+ }
+}
+
+export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses]
+
export type SessionShellData = {
body?: {
agent: string
@@ -1648,6 +1694,22 @@ export type PostSessionByIdPermissionsByPermissionIdResponses = {
export type PostSessionByIdPermissionsByPermissionIdResponse =
PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses]
+export type CommandListData = {
+ body?: never
+ path?: never
+ query?: never
+ url: "/command"
+}
+
+export type CommandListResponses = {
+ /**
+ * List of commands
+ */
+ 200: Array<Command>
+}
+
+export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
+
export type ConfigProvidersData = {
body?: never
path?: never
diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml
index e0c040ec6..3dd34a413 100644
--- a/packages/sdk/stainless/stainless.yml
+++ b/packages/sdk/stainless/stainless.yml
@@ -85,6 +85,12 @@ resources:
methods:
get: get /config
+ command:
+ models:
+ command: Command
+ methods:
+ list: get /command
+
session:
models:
session: Session
@@ -126,6 +132,7 @@ resources:
message: get /session/{id}/message/{messageID}
messages: get /session/{id}/message
chat: post /session/{id}/message
+ command: post /session/{id}/command
shell: post /session/{id}/shell
update: patch /session/{id}
revert: post /session/{id}/revert
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 0c703c959..ecf95ff98 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -84,6 +84,10 @@ type SendPrompt = Prompt
type SendShell = struct {
Command string
}
+type SendCommand = struct {
+ Command string
+ Args string
+}
type SetEditorContentMsg struct {
Text string
}
@@ -183,6 +187,11 @@ func New(
slog.Debug("Loaded config", "config", configInfo)
+ customCommands, err := httpClient.Command.List(ctx)
+ if err != nil {
+ return nil, err
+ }
+
app := &App{
Info: appInfo,
Agents: agents,
@@ -194,7 +203,7 @@ func New(
AgentIndex: agentIndex,
Session: &opencode.Session{},
Messages: []Message{},
- Commands: commands.LoadFromConfig(configInfo),
+ Commands: commands.LoadFromConfig(configInfo, *customCommands),
InitialModel: initialModel,
InitialPrompt: initialPrompt,
InitialAgent: initialAgent,
@@ -793,6 +802,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
return a, tea.Batch(cmds...)
}
+func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) {
+ var cmds []tea.Cmd
+ if a.Session.ID == "" {
+ session, err := a.CreateSession(ctx)
+ if err != nil {
+ return a, toast.NewErrorToast(err.Error())
+ }
+ a.Session = session
+ cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
+ }
+
+ cmds = append(cmds, func() tea.Msg {
+ _, err := a.Client.Session.Command(
+ context.Background(),
+ a.Session.ID,
+ opencode.SessionCommandParams{
+ Command: opencode.F(command),
+ Arguments: opencode.F(args),
+ },
+ )
+ if err != nil {
+ slog.Error("Failed to execute command", "error", err)
+ return toast.NewErrorToast("Failed to execute command")
+ }
+ return nil
+ })
+
+ // The actual response will come through SSE
+ // For now, just return success
+ return a, tea.Batch(cmds...)
+}
+
func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
var cmds []tea.Cmd
if a.Session.ID == "" {
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index bd5d61b95..3a5287ca3 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -31,6 +31,7 @@ type Command struct {
Description string
Keybindings []Keybinding
Trigger []string
+ Custom bool
}
func (c Command) Keys() []string {
@@ -96,6 +97,7 @@ func (r CommandRegistry) Sorted() []Command {
})
return commands
}
+
func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
var matched []Command
for _, command := range r.Sorted() {
@@ -182,7 +184,7 @@ func parseBindings(bindings ...string) []Keybinding {
return parsedBindings
}
-func LoadFromConfig(config *opencode.Config) CommandRegistry {
+func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command) CommandRegistry {
defaults := []Command{
{
Name: AppHelpCommand,
@@ -400,6 +402,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
}
registry[command.Name] = command
}
+ for _, command := range customCommands {
+ registry[CommandName(command.Name)] = Command{
+ Name: CommandName(command.Name),
+ Description: command.Description,
+ Trigger: []string{command.Name},
+ Keybindings: []Keybinding{},
+ Custom: true,
+ }
+ }
+
slog.Info("Loaded commands", "commands", registry)
return registry
}
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index c5ecdc21d..0c52ca84c 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -224,10 +224,17 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.CompletionSelectedMsg:
switch msg.Item.ProviderID {
case "commands":
- commandName := strings.TrimPrefix(msg.Item.Value, "/")
+ command := msg.Item.RawData.(commands.Command)
+ if command.Custom {
+ m.SetValue("/" + command.PrimaryTrigger() + " ")
+ return m, nil
+ }
+
updated, cmd := m.Clear()
m = updated.(*editorComponent)
cmds = append(cmds, cmd)
+
+ commandName := strings.TrimPrefix(msg.Item.Value, "/")
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
return m, tea.Batch(cmds...)
case "files":
@@ -481,6 +488,25 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
}
var cmds []tea.Cmd
+ if strings.HasPrefix(value, "/") {
+ value = value[1:]
+ commandName := strings.Split(value, " ")[0]
+ command := m.app.Commands[commands.CommandName(commandName)]
+ if command.Custom {
+ args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ")
+ cmds = append(
+ cmds,
+ util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}),
+ )
+
+ updated, cmd := m.Clear()
+ m = updated.(*editorComponent)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+ }
+ }
+
attachments := m.textarea.GetAttachments()
prompt := app.Prompt{Text: value, Attachments: attachments}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 97c529721..a299d65af 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -174,6 +174,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
m.tail = true
return m, nil
+ case app.SendCommand:
+ m.viewport.GotoBottom()
+ m.tail = true
+ return m, nil
case dialog.ThemeSelectedMsg:
m.cache.Clear()
m.loading = true
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 26a1ba25a..f7ce7982b 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -408,6 +408,24 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
cmds = append(cmds, cmd)
}
+ case app.SendCommand:
+ // If we're in a child session, switch back to parent before sending prompt
+ if a.app.Session.ParentID != "" {
+ parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
+ if err != nil {
+ slog.Error("Failed to get parent session", "error", err)
+ return a, toast.NewErrorToast("Failed to get parent session")
+ }
+ a.app.Session = parentSession
+ a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
+ cmds = append(cmds, tea.Sequence(
+ util.CmdHandler(app.SessionSelectedMsg(parentSession)),
+ cmd,
+ ))
+ } else {
+ a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
+ cmds = append(cmds, cmd)
+ }
case app.SendShell:
// If we're in a child session, switch back to parent before sending prompt
if a.app.Session.ParentID != "" {
diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs
index 5e58d00e0..b537b79f4 100644
--- a/packages/web/astro.config.mjs
+++ b/packages/web/astro.config.mjs
@@ -67,14 +67,7 @@ export default defineConfig({
{
label: "Usage",
- items: [
- "docs/tui",
- "docs/cli",
- "docs/ide",
- "docs/share",
- "docs/github",
- "docs/gitlab"
- ],
+ items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github", "docs/gitlab"],
},
{
diff --git a/packages/web/src/content/docs/docs/commands.mdx b/packages/web/src/content/docs/docs/commands.mdx
new file mode 100644
index 000000000..93be346c6
--- /dev/null
+++ b/packages/web/src/content/docs/docs/commands.mdx
@@ -0,0 +1,167 @@
+---
+title: Commands
+description: Create custom commands for repetitive tasks.
+---
+
+Define custom commands to automate repetitive coding tasks.
+
+---
+
+## Create command files
+
+Create markdown files in the `command/` directory to define custom commands.
+
+Create `.opencode/command/test.md`:
+
+```md
+---
+description: Run tests with coverage
+agent: build
+model: anthropic/claude-3-5-sonnet-20241022
+---
+
+Run the full test suite with coverage report and show any failures.
+Focus on the failing tests and suggest fixes.
+```
+
+The frontmatter defines command properties. The content becomes the template.
+
+Use the command by typing `/` followed by the command name.
+
+```bash frame="none"
+"/test"
+```
+
+---
+
+## Create command files
+
+For complex commands, create markdown files in the `command/` directory.
+
+Create `.opencode/command/test.md`:
+
+```md
+---
+description: Run tests with coverage
+agent: build
+model: anthropic/claude-3-5-sonnet-20241022
+---
+
+Run the full test suite with coverage report and show any failures.
+Focus on the failing tests and suggest fixes.
+```
+
+The frontmatter defines command properties. The content becomes the template.
+
+---
+
+## Use arguments
+
+Pass arguments to commands using the `$ARGUMENTS` placeholder.
+
+```md
+---
+description: Create a new component
+---
+
+Create a new React component named $ARGUMENTS with TypeScript support.
+Include proper typing and basic structure.
+```
+
+Run the command with arguments:
+
+```bash frame="none"
+"/component Button"
+```
+
+---
+
+## Inject shell output
+
+Use `!`command`` to inject shell command output into your prompt.
+
+```md
+---
+description: Analyze test coverage
+---
+
+Here are the current test results:
+`!npm test`
+
+Based on these results, suggest improvements to increase coverage.
+```
+
+```md
+---
+description: Review recent changes
+---
+
+Recent git commits:
+`!git log --oneline -10`
+
+Review these changes and suggest any improvements.
+```
+
+Commands run in your project's root directory and their output becomes part of the prompt.
+
+---
+
+## Reference files
+
+Include files in your command using `@` followed by the filename.
+
+```md
+---
+description: Review component
+---
+
+Review the component in @src/components/Button.tsx.
+Check for performance issues and suggest improvements.
+```
+
+The file content gets included in the prompt automatically.
+
+---
+
+## Command properties
+
+Configure commands with these optional frontmatter properties:
+
+- **description**: Brief explanation of what the command does
+- **agent**: Agent to use (defaults to "build")
+- **model**: Specific model to use for this command
+
+```md
+---
+description: Code review assistant
+agent: build
+model: anthropic/claude-3-5-sonnet-20241022
+---
+
+Review the code for best practices and suggest improvements.
+```
+
+---
+
+## Command directory
+
+Store command files in these locations:
+
+- `.opencode/command/` - Project-specific commands
+- `command/` - Global commands in config directory
+
+Project commands take precedence over global ones.
+
+---
+
+## Built-in commands
+
+opencode includes several built-in commands:
+
+- `/init` - Initialize project and create AGENTS.md
+- `/undo` - Revert the last changes
+- `/redo` - Restore reverted changes
+- `/share` - Share the current conversation
+- `/help` - Show available commands and keybinds
+
+Use `/help` to see all available commands in your setup.
diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx
index 858a605b2..31587850f 100644
--- a/packages/web/src/content/docs/docs/index.mdx
+++ b/packages/web/src/content/docs/docs/index.mdx
@@ -41,26 +41,10 @@ You can also install it with the following:
- **Using Node.js**
<Tabs>
- <TabItem label="npm">
- ```bash
- npm install -g opencode-ai
- ```
- </TabItem>
- <TabItem label="Bun">
- ```bash
- bun install -g opencode-ai
- ```
- </TabItem>
- <TabItem label="pnpm">
- ```bash
- pnpm install -g opencode-ai
- ```
- </TabItem>
- <TabItem label="Yarn">
- ```bash
- yarn global add opencode-ai
- ```
- </TabItem>
+ <TabItem label="npm">```bash npm install -g opencode-ai ```</TabItem>
+ <TabItem label="Bun">```bash bun install -g opencode-ai ```</TabItem>
+ <TabItem label="pnpm">```bash pnpm install -g opencode-ai ```</TabItem>
+ <TabItem label="Yarn">```bash yarn global add opencode-ai ```</TabItem>
</Tabs>
- **Using Homebrew on macOS and Linux**
@@ -308,4 +292,4 @@ Here's an [example conversation](https://opencode.ai/s/4XP1fce5) with opencode.
And that's it! You are now a pro at using opencode.
-To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), or playing around with the [opencode config](/docs/config).
+To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), [creating custom commands](/docs/commands), or playing around with the [opencode config](/docs/config).