Compare commits

..

1 Commits

Author SHA1 Message Date
Dax Raad beb2060be4 allow selecting model and continuing previous session for opencode run 2025-06-19 12:29:57 -04:00
46 changed files with 832 additions and 1218 deletions
+1 -2
View File
@@ -4,7 +4,6 @@ on:
push:
branches:
- dev
- production
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -21,6 +20,6 @@ jobs:
- run: bun install
- run: bun sst deploy --stage=${{ github.ref_name }}
- run: bun sst deploy --stage=dev
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+124 -5
View File
@@ -8,7 +8,6 @@
</a>
</p>
<p align="center">
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
@@ -17,7 +16,9 @@
AI coding agent, built for the terminal.
[![opencode Terminal UI](packages/web/src/assets/themes/opencode.png)](https://opencode.ai)
**Note:** Version 0.1.x is a full rewrite, and we do not have proper documentation for it yet. Should have this out week of June 17th 2025.
[![opencode Terminal UI](screenshot.png)](https://opencode.ai)
### Installation
@@ -33,9 +34,127 @@ paru -S opencode-bin # Arch Linux
> **Note:** Remove versions older than 0.1.x before installing
### Documentation
### Providers
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
The recommended approach is to sign up for Claude Pro or Max, run `opencode auth login`, and select Anthropic. It's the most cost-effective way to use opencode.
opencode is powered by the provider list at [Models.dev](https://models.dev), so you can use `opencode auth login` to configure API keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Anthropic (recommended)
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
│ ○ Azure
│ ○ DeepSeek
│ ○ Groq
│ ...
```
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
### Config
Config is optional and can be placed in the root of your repo or globally in `~/.config/opencode/config.json`. It can be checked in and shared with your team.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json"
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
"autoshare": false,
"autoupdate": true,
}
```
#### Keybinds
You can configure custom keybinds, the values listed below are the defaults.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"keybinds": {
"leader": "ctrl+x",
"help": "<leader>h",
"editor_open": "<leader>e",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_share": "<leader>s",
"session_interrupt": "esc",
"session_compact": "<leader>c",
"tool_details": "<leader>d",
"model_list": "<leader>m",
"theme_list": "<leader>t",
"project_init": "<leader>i",
"input_clear": "ctrl+c",
"input_paste": "ctrl+v",
"input_submit": "enter",
"input_newline": "shift+enter,ctrl+j",
"history_previous": "up",
"history_next": "down",
"messages_page_up": "pgup",
"messages_page_down": "pgdown",
"messages_half_page_up": "ctrl+alt+u",
"messages_half_page_down": "ctrl+alt+d",
"messages_previous": "ctrl+alt+k",
"messages_next": "ctrl+alt+j",
"messages_first": "ctrl+g",
"messages_last": "ctrl+alt+g",
"app_exit": "ctrl+c,<leader>q"
}
}
```
#### MCP
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"mcp": {
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
},
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
}
}
}
```
#### Providers
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider. Remember most popular providers are preloaded from [models.dev](https://models.dev)
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"llama2": {}
}
}
}
}
```
### Contributing
@@ -44,7 +163,7 @@ To run opencode locally you need.
- Bun
- Golang 1.24.x
And run.
To run.
```bash
$ bun install
-1
View File
@@ -1,5 +1,4 @@
{
"$schema": "https://opencode.ai/config.json",
"keybinds": {},
"mcp": {}
}
+1 -1
View File
@@ -120,7 +120,7 @@ export default {
return new Response(
JSON.stringify({
secret,
url: "https://opencode.ai/s/" + short,
url: "https://dev.opencode.ai/s/" + short,
}),
{
headers: { "Content-Type": "application/json" },
+37 -74
View File
@@ -2,127 +2,97 @@
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
"type": "string"
},
"theme": {
"type": "string",
"description": "Theme name to use for the interface"
"type": "string"
},
"keybinds": {
"type": "object",
"properties": {
"leader": {
"type": "string",
"description": "Leader key for keybind combinations"
"type": "string"
},
"help": {
"type": "string",
"description": "Show help dialog"
"type": "string"
},
"editor_open": {
"type": "string",
"description": "Open external editor"
"type": "string"
},
"session_new": {
"type": "string",
"description": "Create a new session"
"type": "string"
},
"session_list": {
"type": "string",
"description": "List all sessions"
"type": "string"
},
"session_share": {
"type": "string",
"description": "Share current session"
"type": "string"
},
"session_interrupt": {
"type": "string",
"description": "Interrupt current session"
"type": "string"
},
"session_compact": {
"type": "string",
"description": "Toggle compact mode for session"
"type": "string"
},
"tool_details": {
"type": "string",
"description": "Show tool details"
"type": "string"
},
"model_list": {
"type": "string",
"description": "List available models"
"type": "string"
},
"theme_list": {
"type": "string",
"description": "List available themes"
"type": "string"
},
"project_init": {
"type": "string",
"description": "Initialize project configuration"
"type": "string"
},
"input_clear": {
"type": "string",
"description": "Clear input field"
"type": "string"
},
"input_paste": {
"type": "string",
"description": "Paste from clipboard"
"type": "string"
},
"input_submit": {
"type": "string",
"description": "Submit input"
"type": "string"
},
"input_newline": {
"type": "string",
"description": "Insert newline in input"
"type": "string"
},
"history_previous": {
"type": "string",
"description": "Navigate to previous history item"
"type": "string"
},
"history_next": {
"type": "string",
"description": "Navigate to next history item"
"type": "string"
},
"messages_page_up": {
"type": "string",
"description": "Scroll messages up by one page"
"type": "string"
},
"messages_page_down": {
"type": "string",
"description": "Scroll messages down by one page"
"type": "string"
},
"messages_half_page_up": {
"type": "string",
"description": "Scroll messages up by half page"
"type": "string"
},
"messages_half_page_down": {
"type": "string",
"description": "Scroll messages down by half page"
"type": "string"
},
"messages_previous": {
"type": "string",
"description": "Navigate to previous message"
"type": "string"
},
"messages_next": {
"type": "string",
"description": "Navigate to next message"
"type": "string"
},
"messages_first": {
"type": "string",
"description": "Navigate to first message"
"type": "string"
},
"messages_last": {
"type": "string",
"description": "Navigate to last message"
"type": "string"
},
"app_exit": {
"type": "string",
"description": "Exit the application"
"type": "string"
}
},
"additionalProperties": false,
"description": "Custom keybind configurations"
"additionalProperties": false
},
"autoshare": {
"type": "boolean",
@@ -237,8 +207,7 @@
"models"
],
"additionalProperties": false
},
"description": "Custom provider configurations and model overrides"
}
},
"mcp": {
"type": "object",
@@ -249,22 +218,19 @@
"properties": {
"type": {
"type": "string",
"const": "local",
"description": "Type of MCP server connection"
"const": "local"
},
"command": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command and arguments to run the MCP server"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Environment variables to set when running the MCP server"
}
}
},
"required": [
@@ -278,12 +244,10 @@
"properties": {
"type": {
"type": "string",
"const": "remote",
"description": "Type of MCP server connection"
"const": "remote"
},
"url": {
"type": "string",
"description": "URL of the remote MCP server"
"type": "string"
}
},
"required": [
@@ -293,8 +257,7 @@
"additionalProperties": false
}
]
},
"description": "MCP (Model Context Protocol) server configurations"
}
}
},
"additionalProperties": false,
+2 -2
View File
@@ -80,9 +80,9 @@ function main() {
// Create symlink to the actual binary
fs.symlinkSync(binaryPath, binScript)
console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
console.log(`OpenCode binary symlinked: ${binScript} -> ${binaryPath}`)
} catch (error) {
console.error("Failed to create opencode binary symlink:", error.message)
console.error("Failed to create OpenCode binary symlink:", error.message)
process.exit(1)
}
}
+2 -2
View File
@@ -9,14 +9,14 @@ import { map, pipe, sortBy, values } from "remeda"
export const AuthCommand = cmd({
command: "auth",
describe: "Manage credentials",
describe: "manage credentials",
builder: (yargs) =>
yargs
.command(AuthLoginCommand)
.command(AuthLogoutCommand)
.command(AuthListCommand)
.demandCommand(),
async handler() { },
async handler() {},
})
export const AuthListCommand = cmd({
+7 -8
View File
@@ -20,16 +20,15 @@ const TOOL: Record<string, [string, string]> = {
list: ["List", UI.Style.TEXT_INFO_BOLD],
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
}
export const RunCommand = cmd({
command: "run [message..]",
describe: "Run opencode with a message",
describe: "Run OpenCode with a message",
builder: (yargs: Argv) => {
return yargs
.positional("message", {
describe: "message to send",
describe: "Message to send",
type: "string",
array: true,
default: [],
@@ -46,7 +45,7 @@ export const RunCommand = cmd({
})
.option("share", {
type: "boolean",
describe: "share the session",
describe: "Share the session",
})
.option("model", {
type: "string",
@@ -90,8 +89,8 @@ export const RunCommand = cmd({
await Session.share(session.id)
UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://opencode.ai/s/" +
session.id.slice(-8),
"~ https://dev.opencode.ai/s/" +
session.id.slice(-8),
)
}
UI.empty()
@@ -109,8 +108,8 @@ export const RunCommand = cmd({
UI.println(
color + `|`,
UI.Style.TEXT_NORMAL +
UI.Style.TEXT_DIM +
` ${type.padEnd(7, " ")}`,
UI.Style.TEXT_DIM +
` ${type.padEnd(7, " ")}`,
"",
UI.Style.TEXT_NORMAL + title,
)
+2 -2
View File
@@ -5,10 +5,10 @@ import { Installation } from "../../installation"
export const UpgradeCommand = {
command: "upgrade [target]",
describe: "upgrade opencode to the latest version or a specific version",
describe: "Upgrade opencode to the latest version or a specific version",
builder: (yargs: Argv) => {
return yargs.positional("target", {
describe: "specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
type: "string",
})
},
+37 -38
View File
@@ -44,9 +44,9 @@ export namespace Config {
export const McpLocal = z
.object({
type: z.literal("local").describe("Type of MCP server connection"),
command: z.string().array().describe("Command and arguments to run the MCP server"),
environment: z.record(z.string(), z.string()).optional().describe("Environment variables to set when running the MCP server"),
type: z.literal("local"),
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.openapi({
ref: "Config.McpLocal",
@@ -54,8 +54,8 @@ export namespace Config {
export const McpRemote = z
.object({
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
type: z.literal("remote"),
url: z.string(),
})
.openapi({
ref: "Config.McpRemote",
@@ -66,42 +66,42 @@ export namespace Config {
export const Keybinds = z
.object({
leader: z.string().optional().describe("Leader key for keybind combinations"),
help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"),
session_interrupt: z.string().optional().describe("Interrupt current session"),
session_compact: z.string().optional().describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"),
project_init: z.string().optional().describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z.string().optional().describe("Navigate to previous history item"),
history_next: z.string().optional().describe("Navigate to next history item"),
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
messages_previous: z.string().optional().describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z.string().optional().describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"),
leader: z.string().optional(),
help: z.string().optional(),
editor_open: z.string().optional(),
session_new: z.string().optional(),
session_list: z.string().optional(),
session_share: z.string().optional(),
session_interrupt: z.string().optional(),
session_compact: z.string().optional(),
tool_details: z.string().optional(),
model_list: z.string().optional(),
theme_list: z.string().optional(),
project_init: z.string().optional(),
input_clear: z.string().optional(),
input_paste: z.string().optional(),
input_submit: z.string().optional(),
input_newline: z.string().optional(),
history_previous: z.string().optional(),
history_next: z.string().optional(),
messages_page_up: z.string().optional(),
messages_page_down: z.string().optional(),
messages_half_page_up: z.string().optional(),
messages_half_page_down: z.string().optional(),
messages_previous: z.string().optional(),
messages_next: z.string().optional(),
messages_first: z.string().optional(),
messages_last: z.string().optional(),
app_exit: z.string().optional(),
})
.openapi({
ref: "Config.Keybinds",
})
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
$schema: z.string().optional(),
theme: z.string().optional(),
keybinds: Keybinds.optional(),
autoshare: z
.boolean()
.optional()
@@ -127,9 +127,8 @@ export namespace Config {
options: z.record(z.any()).optional(),
}),
)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
.optional(),
mcp: z.record(z.string(), Mcp).optional(),
})
.openapi({
ref: "Config.Info",
+16 -16
View File
@@ -36,7 +36,7 @@ const cli = yargs(hideBin(process.argv))
.usage("\n" + UI.logo())
.command({
command: "$0 [project]",
describe: "start opencode TUI",
describe: "Start opencode TUI",
builder: (yargs) =>
yargs.positional("project", {
type: "string",
@@ -84,21 +84,21 @@ const cli = yargs(hideBin(process.argv))
},
})
; (async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest()
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => { })
})()
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest()
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => {})
})()
await proc.exited
server.stop()
@@ -41,10 +41,6 @@ export namespace Installation {
return VERSION.startsWith("0.0.0")
}
export function isDev() {
return VERSION === "dev"
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
+1 -2
View File
@@ -584,7 +584,7 @@ export namespace Session {
break
case "tool-call-delta":
continue
break
// for some reason ai sdk claims to not send this part but it does
// @ts-expect-error
@@ -619,7 +619,6 @@ export namespace Session {
l.info("unhandled", {
type: value.type,
})
continue
}
await updateMessage(next)
}
+1 -5
View File
@@ -1,6 +1,5 @@
import { App } from "../app/app"
import { Bus } from "../bus"
import { Installation } from "../installation"
import { Session } from "../session"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
@@ -57,10 +56,7 @@ export namespace Share {
}
export const URL =
process.env["OPENCODE_API"] ??
(Installation.isSnapshot() || Installation.isDev()
? "https://api.dev.opencode.ai"
: "https://api.opencode.ai")
process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai"
export async function create(sessionID: string) {
return fetch(`${URL}/share_create`, {
+1 -4
View File
@@ -34,16 +34,13 @@ export const TaskTool = Tool.define({
}
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
if (evt.properties.info.metadata.sessionID !== session.id) return
if (evt.properties.info.metadata.sessionID !== ctx.sessionID) return
ctx.metadata({
title: params.description,
summary: summary(evt.properties.info),
})
})
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
const result = await Session.chat({
sessionID: session.id,
modelID: metadata.modelID,
+1 -1
View File
@@ -1,5 +1,5 @@
- Allows opencode to search the web and use the results to inform responses
- Allows OpenCode to search the web and use the results to inform responses
- Provides up-to-date information for current events and recent data
- Returns search result information formatted as search result blocks
- Use this tool for accessing information beyond Claude's knowledge cutoff
@@ -110,6 +110,9 @@ func (m *editorComponent) Content() string {
PaddingTop(1).
PaddingBottom(1).
Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder(), false, true).
BorderForeground(t.BackgroundElement()).
BorderBackground(t.Background()).
Render(textarea)
hint := base("enter") + muted(" send ")
@@ -1,7 +1,6 @@
package chat
import (
"encoding/json"
"fmt"
"path/filepath"
"slices"
@@ -253,7 +252,6 @@ func renderToolInvocation(
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
showDetails bool,
isLast bool,
contentOnly bool,
) string {
ignoredTools := []string{"todoread"}
if slices.Contains(ignoredTools, toolCall.ToolName) {
@@ -307,19 +305,14 @@ func renderToolInvocation(
toolArgsMap := make(map[string]any)
if toolCall.Args != nil {
value := *toolCall.Args
if m, ok := value.(map[string]any); ok {
m, ok := value.(map[string]any)
if ok {
toolArgsMap = m
keys := make([]string, 0, len(toolArgsMap))
for key := range toolArgsMap {
keys = append(keys, key)
}
slices.Sort(keys)
firstKey := ""
if len(keys) > 0 {
firstKey = keys[0]
for key := range toolArgsMap {
firstKey = key
break
}
toolArgs = renderArgs(&toolArgsMap, firstKey)
}
}
@@ -344,11 +337,22 @@ func renderToolInvocation(
}
}
elapsed := ""
start := metadata.Time.Start
end := metadata.Time.End
durationMs := end - start
duration := time.Duration(durationMs * float32(time.Millisecond))
roundedDuration := time.Duration(duration.Round(time.Millisecond))
if durationMs > 1000 {
roundedDuration = time.Duration(duration.Round(time.Second))
}
elapsed = styles.Muted().Render(roundedDuration.String())
title := ""
switch toolCall.ToolName {
case "read":
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("READ %s", toolArgs)
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed)
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
@@ -356,7 +360,7 @@ func renderToolInvocation(
}
case "edit":
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("EDIT %s", relative(filename))
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed)
if d, ok := metadata.Get("diff"); ok {
patch := d.(string)
var formattedDiff string
@@ -397,14 +401,14 @@ func renderToolInvocation(
}
case "write":
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("WRITE %s", relative(filename))
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed)
if content, ok := toolArgsMap["content"].(string); ok {
body = renderFile(filename, content)
}
}
case "bash":
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("SHELL %s", description)
title = fmt.Sprintf("SHELL %s %s", description, elapsed)
}
if stdout, ok := metadata.Get("stdout"); ok {
command := toolArgsMap["command"].(string)
@@ -415,7 +419,7 @@ func renderToolInvocation(
}
case "webfetch":
toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("FETCH %s", toolArgs)
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed)
if format, ok := toolArgsMap["format"].(string); ok {
body = *result
body = truncateHeight(body, 10)
@@ -425,7 +429,7 @@ func renderToolInvocation(
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "todowrite":
title = fmt.Sprintf("PLAN")
title = fmt.Sprintf("PLAN %s", elapsed)
if to, ok := metadata.Get("todos"); ok && finished {
todos := to.([]any)
@@ -444,60 +448,14 @@ func renderToolInvocation(
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "task":
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("TASK %s", description)
if summary, ok := metadata.Get("summary"); ok {
toolcalls := summary.([]any)
// toolcalls :=
steps := []string{}
for _, toolcall := range toolcalls {
call := toolcall.(map[string]any)
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
data, _ := json.Marshal(toolInvocation)
var toolCall client.MessageToolInvocationToolCall
_ = json.Unmarshal(data, &toolCall)
if metadata, ok := call["metadata"].(map[string]any); ok {
data, _ = json.Marshal(metadata)
var toolMetadata client.MessageInfo_Metadata_Tool_AdditionalProperties
_ = json.Unmarshal(data, &toolMetadata)
step := renderToolInvocation(
toolCall,
nil,
toolMetadata,
false,
false,
true,
)
steps = append(steps, step)
}
}
}
body = strings.Join(steps, "\n")
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
}
default:
toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s %s", toolName, toolArgs)
if result == nil {
empty := ""
result = &empty
}
title = fmt.Sprintf("%s %s %s", toolName, toolArgs, elapsed)
body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
if contentOnly {
title = "∟ " + title
return title
}
if !showDetails {
title = "∟ " + title
padding := calculatePadding()
@@ -538,6 +496,8 @@ func renderToolInvocation(
func renderToolName(name string) string {
switch name {
// case agent.AgentToolName:
// return "Task"
case "list":
return "LIST"
case "webfetch":
@@ -597,8 +557,8 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
func renderToolAction(name string) string {
switch name {
case "task":
return "Searching..."
// case agent.AgentToolName:
// return "Preparing prompt..."
case "bash":
return "Building command..."
case "edit":
@@ -629,17 +589,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
if args == nil || len(*args) == 0 {
return ""
}
keys := make([]string, 0, len(*args))
for key := range *args {
keys = append(keys, key)
}
slices.Sort(keys)
title := ""
parts := []string{}
for _, key := range keys {
value := (*args)[key]
for key, value := range *args {
if value == nil {
continue
}
@@ -191,7 +191,6 @@ func (m *messagesComponent) renderView() {
metadata,
m.showToolDetails,
isLastToolInvocation,
false,
)
m.cache.Set(key, content)
}
@@ -203,7 +202,6 @@ func (m *messagesComponent) renderView() {
metadata,
m.showToolDetails,
isLastToolInvocation,
false,
)
}
@@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/styles"
@@ -202,6 +203,13 @@ func (c *completionDialogComponent) View() string {
return baseStyle.Padding(0, 0).
Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder()).
BorderTop(false).
BorderBottom(false).
BorderRight(true).
BorderLeft(true).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundElement()).
Width(c.width).
Render(c.list.View())
}
@@ -100,9 +100,9 @@ func (m *Modal) Render(contentView string, background string) string {
if m.title != "" {
titleStyle := baseStyle.
Foreground(t.Primary()).
Bold(true).
Padding(0, 1)
Bold(true)
// titleView := titleStyle.Render(m.title)
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
escText := escStyle.Render("esc")
@@ -123,7 +123,14 @@ func (m *Modal) Render(contentView string, background string) string {
PaddingTop(1).
PaddingBottom(1).
PaddingLeft(2).
PaddingRight(2)
PaddingRight(2).
BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(true).
BorderLeftForeground(t.BackgroundSubtle()).
BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundSubtle()).
BorderRightBackground(t.Background())
modalView := modalStyle.
Width(outerWidth).
@@ -143,7 +150,5 @@ func (m *Modal) Render(contentView string, background string) string {
row,
modalView,
background,
layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(t.Primary()),
)
}
+24 -19
View File
@@ -93,7 +93,12 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
baseStyle := styles.BaseStyle().
Background(t.BackgroundElement()).
Foreground(t.Text()).
Padding(1, 2)
Padding(1, 2).
BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()).
BorderForeground(toast.Color).
BorderLeft(true).
BorderRight(true)
maxWidth := max(40, layout.Current.Viewport.Width/3)
contentMaxWidth := max(maxWidth-6, 20)
@@ -132,7 +137,9 @@ func (tm *ToastManager) View() string {
toastViews = append(toastViews, toastView+"\n")
}
return strings.Join(toastViews, "\n")
t := theme.CurrentTheme()
content := lipgloss.JoinVertical(lipgloss.Right, toastViews...)
return lipgloss.NewStyle().Background(t.Background()).Render(content)
}
// RenderOverlay renders the toasts as an overlay on the given background
@@ -144,40 +151,38 @@ func (tm *ToastManager) RenderOverlay(background string) string {
bgWidth := lipgloss.Width(background)
bgHeight := lipgloss.Height(background)
result := background
// Start from top with 2 character padding
currentY := 2
// Render each toast individually
for _, toast := range tm.toasts {
// Render individual toast
toastView := tm.renderSingleToast(toast)
toastWidth := lipgloss.Width(toastView)
toastHeight := lipgloss.Height(toastView)
// Position at top-right with 2 character padding from right edge
x := max(bgWidth-toastWidth-4, 0)
x := bgWidth - toastWidth - 2
// Ensure we don't go negative
if x < 0 {
x = 0
}
// Check if toast fits vertically
if currentY+toastHeight > bgHeight-2 {
if currentY + toastHeight > bgHeight - 2 {
// No more room for toasts
break
}
// Place this toast
result = layout.PlaceOverlay(
x,
currentY,
toastView,
result,
layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(toast.Color),
)
result = layout.PlaceOverlay(x, currentY, toastView, result)
// Move down for next toast (add 1 for spacing between toasts)
currentY += toastHeight + 1
}
return result
}
+16 -249
View File
@@ -1,13 +1,9 @@
package layout
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
chAnsi "github.com/charmbracelet/x/ansi"
"github.com/muesli/ansi"
"github.com/muesli/reflow/truncate"
@@ -27,58 +23,29 @@ func getLines(s string) (lines []string, widest int) {
return lines, widest
}
// overlayOptions holds configuration for overlay rendering
type overlayOptions struct {
whitespace *whitespace
border bool
borderColor *compat.AdaptiveColor
}
// OverlayOption sets options for overlay rendering
type OverlayOption func(*overlayOptions)
// PlaceOverlay places fg on top of bg.
func PlaceOverlay(
x, y int,
fg, bg string,
opts ...OverlayOption,
opts ...WhitespaceOption,
) string {
fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines)
fgHeight := len(fgLines)
// Parse options
options := &overlayOptions{
whitespace: &whitespace{},
if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = util.Clamp(x, 0, bgWidth-fgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
ws := &whitespace{}
for _, opt := range opts {
opt(options)
}
// Adjust for borders if enabled
if options.border {
// Add space for left and right borders
adjustedFgWidth := fgWidth + 2
// Adjust placement to account for borders
x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
// Pad all foreground lines to the same width for consistent borders
for i := range fgLines {
lineWidth := ansi.PrintableRuneWidth(fgLines[i])
if lineWidth < fgWidth {
fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
}
}
} else {
if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = util.Clamp(x, 0, bgWidth-fgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
opt(ws)
}
var b strings.Builder
@@ -92,62 +59,25 @@ func PlaceOverlay(
}
pos := 0
// Handle left side of the line up to the overlay
if x > 0 {
left := truncate.String(bgLine, uint(x))
pos = ansi.PrintableRuneWidth(left)
b.WriteString(left)
if pos < x {
b.WriteString(options.whitespace.render(x - pos))
b.WriteString(ws.render(x - pos))
pos = x
}
}
// Render the overlay content with optional borders
if options.border {
// Get the foreground line
fgLine := fgLines[i-y]
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
// Extract the styles at the border positions
leftStyle := getStyleAtPosition(bgLine, pos)
rightStyle := getStyleAtPosition(bgLine, pos + 1 + fgLineWidth)
// Left border - combine background from original with border foreground
leftSeq := combineStyles(leftStyle, options.borderColor)
if leftSeq != "" {
b.WriteString(leftSeq)
}
b.WriteString("┃")
b.WriteString("\x1b[0m") // Reset all styles
pos++
fgLine := fgLines[i-y]
b.WriteString(fgLine)
pos += ansi.PrintableRuneWidth(fgLine)
// Content
b.WriteString(fgLine)
pos += fgLineWidth
// Right border - combine background from original with border foreground
rightSeq := combineStyles(rightStyle, options.borderColor)
if rightSeq != "" {
b.WriteString(rightSeq)
}
b.WriteString("┃")
b.WriteString("\x1b[0m") // Reset all styles
pos++
} else {
// No border, just render the content
fgLine := fgLines[i-y]
b.WriteString(fgLine)
pos += ansi.PrintableRuneWidth(fgLine)
}
// Handle right side of the line after the overlay
right := cutLeft(bgLine, pos)
bgWidth := ansi.PrintableRuneWidth(bgLine)
rightWidth := ansi.PrintableRuneWidth(right)
if rightWidth <= bgWidth-pos {
b.WriteString(options.whitespace.render(bgWidth - rightWidth - pos))
b.WriteString(ws.render(bgWidth - rightWidth - pos))
}
b.WriteString(right)
@@ -162,146 +92,6 @@ func cutLeft(s string, cutWidth int) string {
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
}
// ansiStyle represents parsed ANSI style attributes
type ansiStyle struct {
fgColor string
bgColor string
attrs []string
}
// parseANSISequence parses an ANSI escape sequence into its components
func parseANSISequence(seq string) ansiStyle {
style := ansiStyle{}
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
return style
}
params := seq[2 : len(seq)-1]
if params == "" {
return style
}
parts := strings.Split(params, ";")
i := 0
for i < len(parts) {
switch parts[i] {
case "0": // Reset
style = ansiStyle{}
case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
style.attrs = append(style.attrs, parts[i])
case "38": // Foreground color
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
// 256 color mode
style.fgColor = strings.Join(parts[i:i+3], ";")
i += 2
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
// RGB color mode
style.fgColor = strings.Join(parts[i:i+5], ";")
i += 4
}
case "48": // Background color
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
// 256 color mode
style.bgColor = strings.Join(parts[i:i+3], ";")
i += 2
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
// RGB color mode
style.bgColor = strings.Join(parts[i:i+5], ";")
i += 4
}
case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
style.fgColor = parts[i]
case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
style.bgColor = parts[i]
case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
style.fgColor = parts[i]
case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
style.bgColor = parts[i]
}
i++
}
return style
}
// combineStyles creates an ANSI sequence that combines background from one style with foreground from another
func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
return ""
}
var parts []string
// Add attributes
parts = append(parts, bgStyle.attrs...)
// Add background color from the original style
if bgStyle.bgColor != "" {
parts = append(parts, bgStyle.bgColor)
}
// Add foreground color if specified
if fgColor != nil {
// Use the light color (could be improved to detect terminal background)
color := (*fgColor).Light
// Use RGBA to get color components
r, g, b, _ := color.RGBA()
// RGBA returns 16-bit values, we need 8-bit
parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
}
if len(parts) == 0 {
return ""
}
return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
}
// getStyleAtPosition extracts the active ANSI style at a given visual position
func getStyleAtPosition(s string, targetPos int) ansiStyle {
// ANSI escape sequence regex
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
visualPos := 0
currentStyle := ansiStyle{}
i := 0
for i < len(s) && visualPos <= targetPos {
// Check if we're at an ANSI escape sequence
if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
// Found an ANSI sequence at current position
seq := s[i : i+match[1]]
parsedStyle := parseANSISequence(seq)
// Update current style (merge with existing)
if parsedStyle.fgColor != "" {
currentStyle.fgColor = parsedStyle.fgColor
}
if parsedStyle.bgColor != "" {
currentStyle.bgColor = parsedStyle.bgColor
}
if len(parsedStyle.attrs) > 0 {
currentStyle.attrs = parsedStyle.attrs
}
i += match[1]
} else if i < len(s) {
// Regular character
if visualPos == targetPos {
return currentStyle
}
_, size := utf8.DecodeRuneInString(s[i:])
i += size
visualPos++
}
}
return currentStyle
}
type whitespace struct {
style termenv.Style
chars string
@@ -339,26 +129,3 @@ func (w whitespace) render(width int) string {
// WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace)
// WithWhitespace sets whitespace options for the overlay
func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
return func(o *overlayOptions) {
for _, opt := range opts {
opt(o.whitespace)
}
}
}
// WithOverlayBorder enables border rendering for the overlay
func WithOverlayBorder() OverlayOption {
return func(o *overlayOptions) {
o.border = true
}
}
// WithOverlayBorderColor sets the border color for the overlay
func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
return func(o *overlayOptions) {
o.borderColor = &color
}
}
+1 -1
View File
@@ -8,6 +8,6 @@ var Terminal *TerminalInfo
func init() {
Terminal = &TerminalInfo{
BackgroundIsDark: true,
BackgroundIsDark: false,
}
}
+3 -15
View File
@@ -212,11 +212,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false
a.completions.SetProvider(a.completionManager.DefaultProvider())
case client.EventInstallationUpdated:
return a, toast.NewSuccessToast(
"New version installed",
toast.WithTitle("opencode updated to "+msg.Properties.Version+", restart to apply."),
)
case client.EventSessionUpdated:
if msg.Properties.Info.Id == a.app.Session.Id {
a.app.Session = &msg.Properties.Info
@@ -234,12 +229,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
}
}
case client.EventSessionError:
unknownError, err := msg.Properties.Error.AsUnknownError()
if err == nil {
slog.Error("Server error", "name", unknownError.Name, "message", unknownError.Data.Message)
return a, toast.NewErrorToast(unknownError.Data.Message, toast.WithTitle(unknownError.Name))
}
case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height
@@ -254,13 +243,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
a.layout.SetSize(a.width, a.height)
case app.SessionSelectedMsg:
messages, err := a.app.ListMessages(context.Background(), msg.Id)
var err error
a.app.Session = msg
a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
if err != nil {
slog.Error("Failed to list messages", "error", err)
return a, toast.NewErrorToast("Failed to open session")
}
a.app.Session = msg
a.app.Messages = messages
case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider
a.app.Model = &msg.Model
+65 -102
View File
@@ -525,9 +525,6 @@
{
"$ref": "#/components/schemas/Event.storage.write"
},
{
"$ref": "#/components/schemas/Event.installation.updated"
},
{
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
},
@@ -540,6 +537,9 @@
{
"$ref": "#/components/schemas/Event.message.part.updated"
},
{
"$ref": "#/components/schemas/Event.installation.updated"
},
{
"$ref": "#/components/schemas/Event.session.updated"
},
@@ -551,11 +551,11 @@
"propertyName": "type",
"mapping": {
"storage.write": "#/components/schemas/Event.storage.write",
"installation.updated": "#/components/schemas/Event.installation.updated",
"lsp.client.diagnostics": "#/components/schemas/Event.lsp.client.diagnostics",
"permission.updated": "#/components/schemas/Event.permission.updated",
"message.updated": "#/components/schemas/Event.message.updated",
"message.part.updated": "#/components/schemas/Event.message.part.updated",
"installation.updated": "#/components/schemas/Event.installation.updated",
"session.updated": "#/components/schemas/Event.session.updated",
"session.error": "#/components/schemas/Event.session.error"
}
@@ -586,30 +586,6 @@
"properties"
]
},
"Event.installation.updated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "installation.updated"
},
"properties": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
]
}
},
"required": [
"type",
"properties"
]
},
"Event.lsp.client.diagnostics": {
"type": "object",
"properties": {
@@ -1225,6 +1201,30 @@
"properties"
]
},
"Event.installation.updated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "installation.updated"
},
"properties": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
]
}
},
"required": [
"type",
"properties"
]
},
"Event.session.updated": {
"type": "object",
"properties": {
@@ -1391,16 +1391,13 @@
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
"type": "string"
},
"theme": {
"type": "string",
"description": "Theme name to use for the interface"
"type": "string"
},
"keybinds": {
"$ref": "#/components/schemas/Config.Keybinds",
"description": "Custom keybind configurations"
"$ref": "#/components/schemas/Config.Keybinds"
},
"autoshare": {
"type": "boolean",
@@ -1511,8 +1508,7 @@
"required": [
"models"
]
},
"description": "Custom provider configurations and model overrides"
}
},
"mcp": {
"type": "object",
@@ -1532,8 +1528,7 @@
"remote": "#/components/schemas/Config.McpRemote"
}
}
},
"description": "MCP (Model Context Protocol) server configurations"
}
}
}
},
@@ -1541,112 +1536,85 @@
"type": "object",
"properties": {
"leader": {
"type": "string",
"description": "Leader key for keybind combinations"
"type": "string"
},
"help": {
"type": "string",
"description": "Show help dialog"
"type": "string"
},
"editor_open": {
"type": "string",
"description": "Open external editor"
"type": "string"
},
"session_new": {
"type": "string",
"description": "Create a new session"
"type": "string"
},
"session_list": {
"type": "string",
"description": "List all sessions"
"type": "string"
},
"session_share": {
"type": "string",
"description": "Share current session"
"type": "string"
},
"session_interrupt": {
"type": "string",
"description": "Interrupt current session"
"type": "string"
},
"session_compact": {
"type": "string",
"description": "Toggle compact mode for session"
"type": "string"
},
"tool_details": {
"type": "string",
"description": "Show tool details"
"type": "string"
},
"model_list": {
"type": "string",
"description": "List available models"
"type": "string"
},
"theme_list": {
"type": "string",
"description": "List available themes"
"type": "string"
},
"project_init": {
"type": "string",
"description": "Initialize project configuration"
"type": "string"
},
"input_clear": {
"type": "string",
"description": "Clear input field"
"type": "string"
},
"input_paste": {
"type": "string",
"description": "Paste from clipboard"
"type": "string"
},
"input_submit": {
"type": "string",
"description": "Submit input"
"type": "string"
},
"input_newline": {
"type": "string",
"description": "Insert newline in input"
"type": "string"
},
"history_previous": {
"type": "string",
"description": "Navigate to previous history item"
"type": "string"
},
"history_next": {
"type": "string",
"description": "Navigate to next history item"
"type": "string"
},
"messages_page_up": {
"type": "string",
"description": "Scroll messages up by one page"
"type": "string"
},
"messages_page_down": {
"type": "string",
"description": "Scroll messages down by one page"
"type": "string"
},
"messages_half_page_up": {
"type": "string",
"description": "Scroll messages up by half page"
"type": "string"
},
"messages_half_page_down": {
"type": "string",
"description": "Scroll messages down by half page"
"type": "string"
},
"messages_previous": {
"type": "string",
"description": "Navigate to previous message"
"type": "string"
},
"messages_next": {
"type": "string",
"description": "Navigate to next message"
"type": "string"
},
"messages_first": {
"type": "string",
"description": "Navigate to first message"
"type": "string"
},
"messages_last": {
"type": "string",
"description": "Navigate to last message"
"type": "string"
},
"app_exit": {
"type": "string",
"description": "Exit the application"
"type": "string"
}
}
},
@@ -1755,22 +1723,19 @@
"properties": {
"type": {
"type": "string",
"const": "local",
"description": "Type of MCP server connection"
"const": "local"
},
"command": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command and arguments to run the MCP server"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Environment variables to set when running the MCP server"
}
}
},
"required": [
@@ -1783,12 +1748,10 @@
"properties": {
"type": {
"type": "string",
"const": "remote",
"description": "Type of MCP server connection"
"const": "remote"
},
"url": {
"type": "string",
"description": "URL of the remote MCP server"
"type": "string"
}
},
"required": [
+61 -129
View File
@@ -41,7 +41,6 @@ type AppInfo struct {
// ConfigInfo defines model for Config.Info.
type ConfigInfo struct {
// Schema JSON schema reference for configuration validation
Schema *string `json:"$schema,omitempty"`
// Autoshare Share newly created sessions automatically
@@ -51,16 +50,12 @@ type ConfigInfo struct {
Autoupdate *bool `json:"autoupdate,omitempty"`
// DisabledProviders Disable providers that are loaded automatically
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
// Mcp MCP (Model Context Protocol) server configurations
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
// Model Model to use in the format of provider/model, eg anthropic/claude-2
Model *string `json:"model,omitempty"`
// Provider Custom provider configurations and model overrides
Model *string `json:"model,omitempty"`
Provider *map[string]struct {
Api *string `json:"api,omitempty"`
Env *[]string `json:"env,omitempty"`
@@ -86,8 +81,6 @@ type ConfigInfo struct {
Npm *string `json:"npm,omitempty"`
Options *map[string]interface{} `json:"options,omitempty"`
} `json:"provider,omitempty"`
// Theme Theme name to use for the interface
Theme *string `json:"theme,omitempty"`
}
@@ -98,107 +91,46 @@ type ConfigInfo_Mcp_AdditionalProperties struct {
// ConfigKeybinds defines model for Config.Keybinds.
type ConfigKeybinds struct {
// AppExit Exit the application
AppExit *string `json:"app_exit,omitempty"`
// EditorOpen Open external editor
EditorOpen *string `json:"editor_open,omitempty"`
// Help Show help dialog
Help *string `json:"help,omitempty"`
// HistoryNext Navigate to next history item
HistoryNext *string `json:"history_next,omitempty"`
// HistoryPrevious Navigate to previous history item
HistoryPrevious *string `json:"history_previous,omitempty"`
// InputClear Clear input field
InputClear *string `json:"input_clear,omitempty"`
// InputNewline Insert newline in input
InputNewline *string `json:"input_newline,omitempty"`
// InputPaste Paste from clipboard
InputPaste *string `json:"input_paste,omitempty"`
// InputSubmit Submit input
InputSubmit *string `json:"input_submit,omitempty"`
// Leader Leader key for keybind combinations
Leader *string `json:"leader,omitempty"`
// MessagesFirst Navigate to first message
MessagesFirst *string `json:"messages_first,omitempty"`
// MessagesHalfPageDown Scroll messages down by half page
AppExit *string `json:"app_exit,omitempty"`
EditorOpen *string `json:"editor_open,omitempty"`
Help *string `json:"help,omitempty"`
HistoryNext *string `json:"history_next,omitempty"`
HistoryPrevious *string `json:"history_previous,omitempty"`
InputClear *string `json:"input_clear,omitempty"`
InputNewline *string `json:"input_newline,omitempty"`
InputPaste *string `json:"input_paste,omitempty"`
InputSubmit *string `json:"input_submit,omitempty"`
Leader *string `json:"leader,omitempty"`
MessagesFirst *string `json:"messages_first,omitempty"`
MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"`
// MessagesHalfPageUp Scroll messages up by half page
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
// MessagesLast Navigate to last message
MessagesLast *string `json:"messages_last,omitempty"`
// MessagesNext Navigate to next message
MessagesNext *string `json:"messages_next,omitempty"`
// MessagesPageDown Scroll messages down by one page
MessagesPageDown *string `json:"messages_page_down,omitempty"`
// MessagesPageUp Scroll messages up by one page
MessagesPageUp *string `json:"messages_page_up,omitempty"`
// MessagesPrevious Navigate to previous message
MessagesPrevious *string `json:"messages_previous,omitempty"`
// ModelList List available models
ModelList *string `json:"model_list,omitempty"`
// ProjectInit Initialize project configuration
ProjectInit *string `json:"project_init,omitempty"`
// SessionCompact Toggle compact mode for session
SessionCompact *string `json:"session_compact,omitempty"`
// SessionInterrupt Interrupt current session
SessionInterrupt *string `json:"session_interrupt,omitempty"`
// SessionList List all sessions
SessionList *string `json:"session_list,omitempty"`
// SessionNew Create a new session
SessionNew *string `json:"session_new,omitempty"`
// SessionShare Share current session
SessionShare *string `json:"session_share,omitempty"`
// ThemeList List available themes
ThemeList *string `json:"theme_list,omitempty"`
// ToolDetails Show tool details
ToolDetails *string `json:"tool_details,omitempty"`
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
MessagesLast *string `json:"messages_last,omitempty"`
MessagesNext *string `json:"messages_next,omitempty"`
MessagesPageDown *string `json:"messages_page_down,omitempty"`
MessagesPageUp *string `json:"messages_page_up,omitempty"`
MessagesPrevious *string `json:"messages_previous,omitempty"`
ModelList *string `json:"model_list,omitempty"`
ProjectInit *string `json:"project_init,omitempty"`
SessionCompact *string `json:"session_compact,omitempty"`
SessionInterrupt *string `json:"session_interrupt,omitempty"`
SessionList *string `json:"session_list,omitempty"`
SessionNew *string `json:"session_new,omitempty"`
SessionShare *string `json:"session_share,omitempty"`
ThemeList *string `json:"theme_list,omitempty"`
ToolDetails *string `json:"tool_details,omitempty"`
}
// ConfigMcpLocal defines model for Config.McpLocal.
type ConfigMcpLocal struct {
// Command Command and arguments to run the MCP server
Command []string `json:"command"`
// Environment Environment variables to set when running the MCP server
Command []string `json:"command"`
Environment *map[string]string `json:"environment,omitempty"`
// Type Type of MCP server connection
Type string `json:"type"`
Type string `json:"type"`
}
// ConfigMcpRemote defines model for Config.McpRemote.
type ConfigMcpRemote struct {
// Type Type of MCP server connection
Type string `json:"type"`
// Url URL of the remote MCP server
Url string `json:"url"`
Url string `json:"url"`
}
// Error defines model for Error.
@@ -752,34 +684,6 @@ func (t *Event) MergeEventStorageWrite(v EventStorageWrite) error {
return err
}
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
var body EventInstallationUpdated
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsEventLspClientDiagnostics returns the union data inside the Event as a EventLspClientDiagnostics
func (t Event) AsEventLspClientDiagnostics() (EventLspClientDiagnostics, error) {
var body EventLspClientDiagnostics
@@ -892,6 +796,34 @@ func (t *Event) MergeEventMessagePartUpdated(v EventMessagePartUpdated) error {
return err
}
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
var body EventInstallationUpdated
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
var body EventSessionUpdated
+5 -2
View File
@@ -7,6 +7,7 @@ import theme from "toolbeam-docs-theme"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
const discord = "https://discord.gg/sst"
const github = "https://github.com/sst/opencode"
// https://astro.build/config
@@ -27,9 +28,10 @@ export default defineConfig({
integrations: [
solidJs(),
starlight({
title: "opencode",
title: "OpenCode",
expressiveCode: { themes: ["github-light", "github-dark"] },
social: [
{ icon: "discord", label: "Discord", href: discord },
{ icon: "github", label: "GitHub", href: github },
],
editLink: {
@@ -50,7 +52,8 @@ export default defineConfig({
"docs/config",
"docs/models",
"docs/themes",
"docs/keybinds",
"docs/shortcuts",
"docs/lsp-servers",
"docs/mcp-servers",
],
components: {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

+9 -69
View File
@@ -3,7 +3,6 @@ import config from 'virtual:starlight/user-config';
import { Icon } from '@astrojs/starlight/components';
import { HeaderLinks } from 'toolbeam-docs-theme/components';
import Default from 'toolbeam-docs-theme/overrides/Header.astro';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
const path = Astro.url.pathname;
@@ -12,32 +11,19 @@ const links = config.social || [];
---
{ path.startsWith("/s")
? <div class="header sl-flex">
<div class="title-wrapper sl-flex">
<SiteTitle {...Astro.props} />
</div>
<div class="middle-group sl-flex">
<HeaderLinks {...Astro.props} />
</div>
<div class="sl-hidden md:sl-flex right-group">
{
links.length > 0 && (
<div class="sl-flex social-icons">
{links.map(({ href, icon }) => (
<a {href} rel="me" target="_blank">
<Icon name={icon} size="1rem" />
</a>
))}
</div>
)
}
</div>
</div>
? <div class="header sl-flex">
<div class="title-wrapper sl-flex">
<SiteTitle {...Astro.props} />
</div>
<div class="middle-group sl-flex">
<HeaderLinks {...Astro.props} />
</div>
</div>
: <Default {...Astro.props}><slot /></Default>
}
<style>
.header {
gap: var(--sl-nav-gap);
justify-content: space-between;
align-items: center;
height: 100%;
@@ -67,50 +53,4 @@ const links = config.social || [];
display: flex;
}
}
.right-group,
.social-icons {
gap: 1rem;
align-items: center;
a {
line-height: 1;
svg {
color: var(--sl-color-text-dimmed);
}
}
}
@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
}
:global(:root:not([data-has-toc])) {
--__toc-width: 0rem;
}
.header {
--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
--__main-column-fr: calc(
(
100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
(2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
var(--sl-content-width)
) / 2
);
display: grid;
grid-template-columns:
/* 1 (site title): runs up until the main content columns left edge or the width of the title, whichever is the largest */
minmax(
calc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),
auto
)
/* 2 (search box): all free space that is available. */
1fr
/* 3 (right items): use the space that these need. */
auto;
align-content: center;
}
}
</style>
+14 -14
View File
@@ -19,7 +19,7 @@ const imageAttrs = {
const github = config.social.filter(s => s.icon === 'github')[0];
const command = "npm i -g";
const pkg = "opencode-ai";
const pkg = "opencode";
let darkImage: ImageMetadata | undefined;
let lightImage: ImageMetadata | undefined;
@@ -68,12 +68,12 @@ if (image) {
<section class="content">
<ul>
<li><b>Native TUI</b>: A responsive, native, themeable terminal UI.</li>
<li><b>LSP enabled</b>: Automatically loads the right LSPs for the LLM.</li>
<li><b>Multi-session</b>: Start multiple agents in parallel on the same project.</li>
<li><b>Shareable links</b>: Share a link to any sessions for reference or to debug.</li>
<li><b>Claude Pro</b>: Log in with Anthropic to use your Claude Pro or Max account.</li>
<li><b>Use any model</b>: Supports 75+ LLM providers through <a href="https://models.dev">Models.dev</a>, including local models.</li>
<li><b>Native TUI</b>: A native terminal UI for a smoother, snappier experience.</li>
<li><b>LSP enabled</b>: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.</li>
<li><b>Multi-session</b>: Start multiple conversations in a project to have agents working in parallel.</li>
<li><b>Use any model</b>: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.</li>
<li><b>Change tracking</b>: View the file changes from the current conversation in the sidebar.</li>
<li><b>Edit with Vim</b>: Use Vim as an external editor to compose longer messages.</li>
</ul>
</section>
@@ -94,7 +94,7 @@ if (image) {
--heading-font-size: var(--sl-text-3xl);
margin: 1rem;
border: 2px solid var(--sl-color-border);
border: 2px solid var(--sl-color-white);
}
@media (max-width: 30rem) {
.hero {
@@ -126,7 +126,7 @@ section.cta {
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 2px solid var(--sl-color-border);
border-top: 2px solid var(--sl-color-white);
& > div {
flex: 1;
@@ -145,7 +145,7 @@ section.cta {
}
& > div + div {
border-left: 2px solid var(--sl-color-border);
border-left: 2px solid var(--sl-color-white);
}
.command {
@@ -195,7 +195,7 @@ section.cta {
}
section.content {
border-top: 2px solid var(--sl-color-border);
border-top: 2px solid var(--sl-color-white);
padding: var(--padding);
ul {
@@ -212,7 +212,7 @@ section.content {
}
section.approach {
border-top: 2px solid var(--sl-color-border);
border-top: 2px solid var(--sl-color-white);
padding: var(--padding);
p + p {
@@ -221,7 +221,7 @@ section.approach {
}
section.footer {
border-top: 2px solid var(--sl-color-border);
border-top: 2px solid var(--sl-color-white);
display: flex;
flex-direction: row;
@@ -233,7 +233,7 @@ section.footer {
}
& > div + div {
border-left: 2px solid var(--sl-color-border);
border-left: 2px solid var(--sl-color-white);
}
}
</style>
+69 -100
View File
@@ -2,119 +2,88 @@
title: CLI
---
Running the opencode CLI starts it for the current directory.
Once installed you can run the OpenCode CLI.
```bash
opencode
```
Or you can start it for a specific working directory.
Or pass in flags. For example, to start with debug logging:
```bash
opencode /path/to/project
opencode -d
```
---
## Commands
The opencode CLI also has the following commands.
### run
Run opencode in non-interactive mode by passing a prompt directly.
Or start with a specific working directory.
```bash
opencode run [message..]
opencode -c /path/to/project
```
This is useful for scripting, automation, or when you want a quick answer without launching the full TUI. For example.
```bash "opencode run"
opencode run Explain the use of context in Go
```
#### Flags
| Flag | Short | Description |
| ----------------- | ----- | --------------------- |
| `--continue` | `-c` | Continue the last session |
| `--session` | `-s` | Session ID to continue |
| `--share` | | Share the session |
| `--model` | `-m` | Mode to use in the form of provider/model |
---
### auth
Command to manage credentials and login for providers.
```bash
opencode auth [command]
```
#### login
Logs you into a provider and saves them in the credentials file in `~/.local/share/opencode/auth.json`.
```bash
opencode auth login
```
When opencode starts up it will loads the providers from the credentials file. And if there are any keys defined in your environments or a `.env` file in your project.
#### list
Lists all the authenticated providers as stored in the credentials file.
```bash
opencode auth list
```
Or the short version.
```bash
opencode auth ls
```
#### logout
Logs you out of a provider by clearing it from the credentials file.
```bash
opencode auth logout
```
---
### upgrade
Updates opencode to the latest version or a specific version.
```bash
opencode upgrade [target]
```
To upgrade to the latest version.
```bash
opencode upgrade
```
To upgrade to a specific version.
```bash
opencode upgrade v0.1.48
```
---
## Flags
The opencode CLI takes the following flags.
The OpenCode CLI takes the following flags.
| Flag | Short | Description |
| ----------------- | ----- | --------------------- |
| `--help` | `-h` | Display help |
| `--version` | | Print version number |
| `--print-logs` | | Print logs to stderr |
| Flag | Short | Description |
| ----------------- | ----- | -------------------------------------------------------- |
| `--help` | `-h` | Display help |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
## Non-interactive
By default, OpenCode runs in interactive mode.
But you can also run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
For example, to run a single prompt use the `-p` flag.
```bash "-p"
opencode -p "Explain the use of context in Go"
```
If you want to run without showing the spinner, use `-q`.
```bash "-q"
opencode -p "Explain the use of context in Go" -q
```
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All **permissions are auto-approved** for the session.
#### Tool restrictions
You can control which tools the AI assistant has access to in non-interactive mode.
- `--allowedTools`
A comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
```bash "--allowedTools"
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
```
- `--excludedTools`
Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
```bash "--excludedTools"
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
```
These flags are mutually exclusive. So you can either use `--allowedTools` or `--excludedTools`, but not both.
#### Output formats
In non-interactive mode, you can also set the CLI to return as JSON using `-f`.
```bash "-f json"
opencode -p "Explain the use of context in Go" -f json
```
By default, this is set to `text`, to return plain text.
+75 -17
View File
@@ -2,28 +2,86 @@
title: Config
---
You can configure opencode using a JSON config file that can be placed in:
You can configure OpenCode using the OpenCode config. It can be places in:
- Globally under `~/.config/opencode/config.json`.
- Your project root under `opencode.json`. This is safe to checked into Git and uses the same schema as the global one.
- `$HOME/.opencode.json`
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
```json
Or in the current directory, `./.opencode.json`.
## OpenCode config
The config file has the following structure.
```json title=".opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514",
"autoshare": false,
"autoupdate": true
"data": {
"directory": ".opencode"
},
"providers": {
"openai": {
"apiKey": "your-api-key",
"disabled": false
},
"anthropic": {
"apiKey": "your-api-key",
"disabled": false
},
"groq": {
"apiKey": "your-api-key",
"disabled": false
},
"openrouter": {
"apiKey": "your-api-key",
"disabled": false
}
},
"agents": {
"primary": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"task": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"title": {
"model": "claude-3.7-sonnet",
"maxTokens": 80
}
},
"mcpServers": {
"example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
}
},
"lsp": {
"go": {
"disabled": false,
"command": "gopls"
}
},
"debug": false,
"debugLSP": false
}
```
In most cases, you'll want to use the global config for things like themes, providers, or keybinds. Having a config per project is useful if you are using different providers for your company.
## Environment variables
When opencode starts up, it looks for a config file in the current directory or traverse up to the nearest Git directory.
For the providers, you can also specify the keys using environment variables.
## Schema
The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json).
Your editor should be able to validate and autocomplete based on the schema.
| Environment Variable | Models |
| -------------------------- | ------------------------------------------ |
| `ANTHROPIC_API_KEY` | Claude |
| `OPENAI_API_KEY` | OpenAI |
| `GEMINI_API_KEY` | Google Gemini |
| `GROQ_API_KEY` | Groq |
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
| `AWS_REGION` | Amazon Bedrock |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI |
+30 -58
View File
@@ -2,85 +2,57 @@
title: Intro
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
OpenCode is an AI coding agent built natively for the terminal. It features:
[**opencode**](/) is an AI coding agent built for the terminal. It features:
- A responsive, native, themeable terminal UI.
- Automatically loads the right LSPs, so the LLMs make fewer mistakes.
- Have multiple agents working in parallel on the same project.
- Create shareable links to any session for reference or to debug.
- Log in with Anthropic to use your Claude Pro or Claude Max account.
- Supports 75+ LLM providers through [Models.dev](https://models.dev), including local models.
- Native TUI for a smoother, snappier experience
- Uses LSPs to help the LLM make fewer mistakes
- Opening multiple conversations with the same project
- Use of any model through the AI SDK
- Tracks and visualizes all the file changes
- Editing longer messages with Vim
## Installation
<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>
</Tabs>
```bash
npm i -g opencode
```
You can also install the opencode binary through the following.
If you don't have NPM installed, you can also install the OpenCode binary through the following.
##### Using the install script
#### Using the install script
```bash
curl -fsSL https://opencode.ai/install | bash
```
##### Using Homebrew on macOS
Or install a specific version.
```bash
curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
```
#### Using Homebrew on macOS and Linux
```bash
brew install sst/tap/opencode
```
##### Using Paru on Arch Linux
#### Using AUR in Arch Linux
With yay.
```bash
yay -S opencode-bin
```
Or with paru.
```bash
paru -S opencode-bin
```
## Providers
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
#### Using Go
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Anthropic (recommended)
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
│ ○ Azure
│ ○ DeepSeek
│ ○ Groq
│ ...
go install github.com/sst/opencode@latest
```
opencode is powered by the provider list at [Models.dev](https://models.dev), so you can use `opencode auth login` to configure API keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`.
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). You can also [add them to your config](/docs/config) for yourself.
@@ -1,48 +0,0 @@
---
title: Keybinds
---
opencode has a list of keybinds that you can customize through the opencode config.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"keybinds": {
"leader": "ctrl+x",
"help": "<leader>h",
"editor_open": "<leader>e",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_share": "<leader>s",
"session_interrupt": "esc",
"session_compact": "<leader>c",
"tool_details": "<leader>d",
"model_list": "<leader>m",
"theme_list": "<leader>t",
"project_init": "<leader>i",
"input_clear": "ctrl+c",
"input_paste": "ctrl+v",
"input_submit": "enter",
"input_newline": "shift+enter,ctrl+j",
"history_previous": "up",
"history_next": "down",
"messages_page_up": "pgup",
"messages_page_down": "pgdown",
"messages_half_page_up": "ctrl+alt+u",
"messages_half_page_down": "ctrl+alt+d",
"messages_previous": "ctrl+alt+k",
"messages_next": "ctrl+alt+j",
"messages_first": "ctrl+g",
"messages_last": "ctrl+alt+g",
"app_exit": "ctrl+c,<leader>q"
}
}
```
## Leader key
opencode uses a `leader` key for most keybinds. This avoids conflicts in your terminal.
By default, `ctrl+x` is the leader key and most actions require you to first press the leader key and then the shortcut. For example, to start a new session you first press `ctrl+x` and then press `n`.
You don't need to use a leader key for your keybinds but we recommend doing so.
@@ -2,7 +2,7 @@
title: LSP servers
---
opencode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase.
OpenCode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase.
LSP servers for different languages give the LLM:
@@ -11,13 +11,13 @@ LSP servers for different languages give the LLM:
## Auto-detection
By default, opencode will **automatically detect** the languages used in your project and add the right LSP servers.
By default, OpenCode will **automatically detect** the languages used in your project and add the right LSP servers.
## Manual configuration
You can also manually configure LSP servers by adding them under the `lsp` section in your opencode config.
You can also manually configure LSP servers by adding them under the `lsp` section in your OpenCode config.
```json title="opencode.json"
```json title=".opencode.json"
{
"lsp": {
"go": {
@@ -2,33 +2,27 @@
title: MCP servers
---
You can add external tools to opencode using the _Model Context Protocol_, or MCP. opencode supports both:
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both:
- Local servers
- And remote servers
- Local servers that use standard input/output, `stdio`
- Remote servers that use server-sent events `sse`
Once added, MCP tools are automatically available to the LLM alongside built-in tools.
## Add MCP servers
---
## Configure
You can define MCP servers in your opencode config under `mcp`.
You can define MCP servers in your OpenCode config under the `mcpServers` section:
### Local
Add a local MCP servers under `mcp.localmcp`.
To add a local or `stdio` MCP server.
```json title="opencode.json"
```json title=".opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
"mcpServers": {
"local-example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
}
}
}
@@ -36,16 +30,22 @@ Add a local MCP servers under `mcp.localmcp`.
### Remote
Add a remote MCP servers under `mcp.remotemcp`.
To add a remote or `sse` MCP server.
```json title="opencode.json"
```json title=".opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
"mcpServers": {
"remote-example": {
"type": "sse",
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer token"
}
}
}
}
```
## Usage
Once added, MCP tools are automatically available to the LLM alongside built-in tools. They follow the same permission model; requiring user approval before execution.
+19 -74
View File
@@ -2,88 +2,33 @@
title: Models
---
opencode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support for **75+ LLM providers** and it supports running local models.
OpenCode uses the [AI SDK](https://ai-sdk.dev/) to have the support for **all the AI models**.
---
Start by setting the [keys for the providers](/docs/config) you want to use in your OpenCode config.
## Providers
## Model select
You can configure providers in your opencode config under the `provider` section.
You can now select the model you want from the menu by hitting `Ctrl+O`.
### Defaults
## Multiple models
Most popular providers are preloaded by default. If you've added the credentials for a provider through `opencode auth login`, they'll be available when you start opencode.
You can also use specific models for specific tasks. For example, you can use a smaller model to generate the title of the conversation or to run a sub task.
### Custom
You can add custom providers by specifying the npm package for the provider and the models you want to use.
```json title="opencode.json" {5,9-11}
```json title=".opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"npm": "@openrouter/ai-sdk-provider",
"name": "OpenRouter",
"options": {},
"models": {
"anthropic/claude-3.5-sonnet": {
"name": "Claude 3.5 Sonnet"
}
}
"agents": {
"primary": {
"model": "gpt-4",
"maxTokens": 5000
},
"task": {
"model": "gpt-3.5-turbo",
"maxTokens": 5000
},
"title": {
"model": "gpt-3.5-turbo",
"maxTokens": 80
}
}
}
```
### Local
To configure a local model, specify the npm package to use and the `baseURL`.
```json title="opencode.json" {5,7}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"llama2": {}
}
}
}
}
```
---
## Select a model
If you have multiple models, you can select the model you want by typing in:
```bash frame="none"
/models
```
---
## Loading models
When opencode starts up, it checks for the following:
1. The model list in the opencode config.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-20250514"
}
```
The format here is `provider/model`.
2. The last used model.
3. The first model using an internal priority.
@@ -0,0 +1,68 @@
---
title: Keyboard shortcuts
sidebar:
label: Shortcuts
---
Below are a list of keyboard shortcuts that OpenCode supports.
## Global
| Shortcut | Action |
| -------- | ------------------------------------------------------- |
| `Ctrl+C` | Quit application |
| `Ctrl+?` | Toggle help dialog |
| `?` | Toggle help dialog (when not in editing mode) |
| `Ctrl+L` | View logs |
| `Ctrl+A` | Switch session |
| `Ctrl+K` | Command dialog |
| `Ctrl+O` | Toggle model selection dialog |
| `Esc` | Close current overlay/dialog or return to previous mode |
## Chat pane
| Shortcut | Action |
| -------- | --------------------------------------- |
| `Ctrl+N` | Create new session |
| `Ctrl+X` | Cancel current operation/generation |
| `i` | Focus editor (when not in writing mode) |
| `Esc` | Exit writing mode and focus messages |
## Editor view
| Shortcut | Action |
| ------------------- | ----------------------------------------- |
| `Ctrl+S` | Send message (when editor is focused) |
| `Enter` or `Ctrl+S` | Send message (when editor is not focused) |
| `Ctrl+E` | Open external editor |
| `Esc` | Blur editor and focus messages |
## Session dialog
| Shortcut | Action |
| ---------- | ---------------- |
| `↑` or `k` | Previous session |
| `↓` or `j` | Next session |
| `Enter` | Select session |
| `Esc` | Close dialog |
## Model dialog
| Shortcut | Action |
| ---------- | ----------------- |
| `↑` or `k` | Move up |
| `↓` or `j` | Move down |
| `←` or `h` | Previous provider |
| `→` or `l` | Next provider |
| `Esc` | Close dialog |
## Permission dialog
| Shortcut | Action |
| ----------------------- | ---------------------------- |
| `←` or `left` | Switch options left |
| `→` or `right` or `tab` | Switch options right |
| `Enter` or `space` | Confirm selection |
| `a` | Allow permission |
| `A` | Allow permission for session |
| `d` | Deny permission |
+59 -32
View File
@@ -2,47 +2,74 @@
title: Themes
---
opencode will support most common terminal themes and you'll soon be able to create your own custom theme.
OpenCode supports most common terminal themes and you can create your own custom theme.
---
## Built-in themes
## Built-in
The following built-in themes are available:
The following predefined themes are available:
- `opencode`
![opencode theme](../../../assets/themes/opencode.png)
- `ayu`
![ayu theme](../../../assets/themes/ayu.png)
- `everforest`
![everforest theme](../../../assets/themes/everforest.png)
- `catppuccin`
- `dracula`
- `flexoki`
- `gruvbox`
- `monokai`
- `onedark`
- `tokyonight`
- `tron`
- `custom`
![tokyonight theme](../../../assets/themes/tokyonight.png)
Where `opencode` is the default theme and `custom` let's you define your own theme.
---
## Setting a theme
## Configure
You can set your theme in your OpenCode config.
To select a theme, type in:
```bash frame="none"
/themes
```
Your selected theme will be used the next time you start opencode.
You can also configure it in your opencode config.
```json title="opencode.json" {3}
```json title=".opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"theme": "tokyonight"
"tui": {
"theme": "monokai"
}
}
```
## Create a theme
You can create your own custom theme by setting the `theme: custom` and providing color definitions through the `customTheme`.
```json title=".opencode.json"
{
"tui": {
"theme": "custom",
"customTheme": {
"primary": "#ffcc00",
"secondary": "#00ccff",
"accent": { "dark": "#aa00ff", "light": "#ddccff" },
"error": "#ff0000"
}
}
}
```
#### Color keys
You can define any of the following color keys in your `customTheme`.
| Type | Color keys |
| ----------------- | ------------------------------------------------------- |
| Base colors | `primary`, `secondary`, `accent` |
| Status colors | `error`, `warning`, `success`, `info` |
| Text colors | `text`, `textMuted` |
| Background colors | `background`, `backgroundSubtle`, `backgroundElement` |
| Border colors | `border`, `borderActive`, `borderSubtle` |
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.
#### Color definitions
Color keys can take:
1. **Hex string**: A single hex color string, like `"#aabbcc"`, that'll be used for both light and dark terminal backgrounds.
2. **Light and dark colors**: An object with `dark` and `light` hex colors that'll be set based on the terminal's background.
+2 -2
View File
@@ -1,5 +1,5 @@
---
title: opencode
title: OpenCode
description: The AI coding agent built for the terminal.
template: splash
hero:
@@ -8,5 +8,5 @@ hero:
image:
dark: ../../assets/logo-dark.svg
light: ../../assets/logo-light.svg
alt: opencode logo
alt: OpenCode logo
---
-7
View File
@@ -42,13 +42,6 @@ const ogImage = `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png
template: "splash",
tableOfContents: false,
head: [
{
tag: "meta",
attrs: {
name: "description",
content: "opencode - The AI coding agent built for the terminal.",
},
},
{
tag: "meta",
attrs: {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB