Compare commits

..

62 Commits

Author SHA1 Message Date
Dax Raad 226a4a7f36 temporary patch for input lag on initial run 2025-06-27 14:36:03 -04:00
Polo123456789 df3b424830 feat: Add golang file formatting (#474) 2025-06-27 14:11:09 -04:00
Wendell Misiedjan 3cfd9d80bc fix: bunproc stdout / stderr parsing, error handling for bun ResolveMessage (#468) 2025-06-27 14:09:35 -04:00
Jay V e0553b8d2c docs: update logo screenshot 2025-06-27 14:04:09 -04:00
Jay V 391c837b37 docs: share page write tool bug 2025-06-27 13:25:15 -04:00
Dax Raad 5773d9d1a3 ci: ignore 2025-06-27 12:37:57 -04:00
Dax Raad ce611963c3 ci: aur 2025-06-27 12:29:13 -04:00
Dax Raad f865cacfb8 ignore: cleanup 2025-06-27 11:35:57 -04:00
Dax Raad 2ec0611f42 lazy load formatters 2025-06-27 11:33:37 -04:00
Ryan Winchester 334161a30e feat: add elixir file formatting (#458) 2025-06-27 10:15:11 -04:00
adamdottv dbb6e55226 fix(web): remove system prompts from share page 2025-06-27 06:48:44 -05:00
TheGoddessInari d0f9260559 scripts/hooks: Change shebang to universal /bin/sh (#453) 2025-06-27 07:40:22 -04:00
adamdottv d2176064e1 fix(tui): min width on user messages 2025-06-27 06:31:13 -05:00
Dax Raad ed8d277e49 fix formatting output going into tui 2025-06-27 07:29:41 -04:00
adamdottv 59b3268c64 ignore: more metadata in app info 2025-06-27 06:19:27 -05:00
adamdottv d043f67761 fix: don't use prettier for langs it doesn't format 2025-06-27 05:47:14 -05:00
Dax Raad 51bf193889 ignore: run prettier 2025-06-26 22:30:44 -04:00
Dax Raad f8b78f08b4 add auto formatting and experimental hooks feature 2025-06-26 22:17:08 -04:00
Jay V a4f32d602b docs: lander tweak 2025-06-26 19:47:58 -04:00
Jay V dc3dd21cf3 docs: tweak lander 2025-06-26 19:02:44 -04:00
Dax Raad 8ca713b737 disable task tool temporarily 2025-06-26 18:27:49 -04:00
Jay V 5b54554fd5 docs: edit theme doc 2025-06-26 17:56:31 -04:00
Dax Raad 4bc651f958 fix: improve JSON formatting and add piped output support for run command
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-26 17:32:00 -04:00
Jay V 3b6976a9c8 Merge branch 'rekram1-node-chore/update-config-docs' into dev 2025-06-26 17:24:03 -04:00
Jay V 863d5c1e8e docs: editing rules 2025-06-26 17:23:52 -04:00
adamdottv 97e19e9677 fix(tui): editor styles were off 2025-06-26 17:22:21 -04:00
adamdottv b27851461f feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv 209687377a feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv 90face1c09 fix(tui): editor width issues 2025-06-26 17:22:21 -04:00
adamdottv 936e2ce48b feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 17:22:21 -04:00
adamdottv 16ee8ee379 fix(tui): chat editor aesthetics 2025-06-26 17:22:21 -04:00
adamdottv ac39308dad fix(tui): visual issue with modal selected items in system theme 2025-06-26 17:22:21 -04:00
adamdottv 346b49219d chore: tui agents.md 2025-06-26 17:22:21 -04:00
Jay V d84c1f20c7 docs: social share 2025-06-26 17:22:17 -04:00
adamdottv dfb8777555 fix(tui): editor spinner colors 2025-06-26 17:21:53 -04:00
Jay V 008af18156 docs: share page responsive diff 2025-06-26 17:21:53 -04:00
adamdottv ab23167f80 docs: system theme 2025-06-26 17:21:53 -04:00
adamdottv b17ec46463 fix(tui): make opencode theme default 2025-06-26 17:21:53 -04:00
Adam 2e26b58d16 feat: default system theme (#419)
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-26 17:21:53 -04:00
Mike Wallio 31b56e5a05 Fix undefined is not an object (evaluating 'G.title') (#395) 2025-06-26 17:21:53 -04:00
Juhani Pelli 47c401cf25 fix: guard against large output limit causing infinite summarize loop (#399) 2025-06-26 17:21:53 -04:00
Dax Raad fab8dc9e6f more edit tool fixes 2025-06-26 17:21:53 -04:00
Dax Raad f39a2b1f16 integrate gemini-cli strategies for edit tool 2025-06-26 17:21:53 -04:00
Dax Raad 66830ced4e make edit tool more robust 2025-06-26 17:21:53 -04:00
Dax Raad 9d3fad754d ignore: typo 2025-06-26 17:21:53 -04:00
Dax Raad dcd3131f58 add output length errors 2025-06-26 17:21:53 -04:00
Dax Raad 3d02e07161 fix codex not working 2025-06-26 17:21:53 -04:00
Dax Raad 4dbc6a43a6 redirect uncaught errors to log file 2025-06-26 17:21:53 -04:00
adamdottv 5394b5188b fix(tui): editor styles were off 2025-06-26 15:12:26 -05:00
adamdottv 8e680b3957 feat(tui): more themes 2025-06-26 15:03:30 -05:00
adamdottv 1b8cd796d6 feat(tui): more themes 2025-06-26 14:54:32 -05:00
adamdottv 35fba793d0 fix(tui): editor width issues 2025-06-26 12:57:11 -05:00
adamdottv 5358d43b74 feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 12:47:17 -05:00
adamdottv f777347bac fix(tui): chat editor aesthetics 2025-06-26 12:44:44 -05:00
adamdottv 17c8b914df fix(tui): visual issue with modal selected items in system theme 2025-06-26 12:33:06 -05:00
adamdottv 43b467dd12 chore: tui agents.md 2025-06-26 12:28:29 -05:00
Jay V 0e0770921e docs: social share 2025-06-26 13:21:42 -04:00
adamdottv 8edbb74352 fix(tui): editor spinner colors 2025-06-26 12:21:20 -05:00
Jay V e6bfa95758 docs: share page responsive diff 2025-06-26 13:06:41 -04:00
adamdottv e4120b6287 docs: system theme 2025-06-26 11:33:02 -05:00
adamdottv ccbc9e00f2 fix(tui): make opencode theme default 2025-06-26 11:32:25 -05:00
rekram1-node 9acc83697f chore: document AGENTS.md 2025-06-26 08:28:06 -05:00
81 changed files with 3778 additions and 632 deletions
+4 -4
View File
@@ -1,9 +1,9 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/web/src/assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/web/src/assets/logo-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/web/src/assets/logo-light.svg" alt="opencode logo">
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
</picture>
</a>
</p>
@@ -14,7 +14,7 @@
<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>
[![opencode Terminal UI](packages/web/src/assets/themes/opencode.png)](https://opencode.ai)
[![opencode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
+1 -1
View File
@@ -19,7 +19,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.0.0",
"version": "0.0.5",
"bin": {
"opencode": "./bin/opencode",
},
+17 -1
View File
@@ -1,3 +1,19 @@
{
"$schema": "https://opencode.ai/config.json"
"$schema": "https://opencode.ai/config.json",
"experimental": {
"hook": {
"file_edited": {
".json": [
{
"command": ["bun", "run", "prettier", "$FILE"]
}
]
},
"session_completed": [
{
"command": ["touch", "./node_modules/foo"]
}
]
}
}
}
+8 -8
View File
@@ -6,20 +6,20 @@
import "sst"
declare module "sst" {
export interface Resource {
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
Web: {
type: "sst.cloudflare.Astro"
url: string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"Bucket": cloudflare.R2Bucket
Api: cloudflare.Service
Bucket: cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}
+72 -20
View File
@@ -183,6 +183,9 @@
"temperature": {
"type": "boolean"
},
"tool_call": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
@@ -199,10 +202,7 @@
"type": "number"
}
},
"required": [
"input",
"output"
],
"required": ["input", "output"],
"additionalProperties": false
},
"limit": {
@@ -215,14 +215,15 @@
"type": "number"
}
},
"required": [
"context",
"output"
],
"required": ["context", "output"],
"additionalProperties": false
},
"id": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"additionalProperties": false
@@ -233,9 +234,7 @@
"additionalProperties": {}
}
},
"required": [
"models"
],
"required": ["models"],
"additionalProperties": false
},
"description": "Custom provider configurations and model overrides"
@@ -267,10 +266,7 @@
"description": "Environment variables to set when running the MCP server"
}
},
"required": [
"type",
"command"
],
"required": ["type", "command"],
"additionalProperties": false
},
{
@@ -286,17 +282,73 @@
"description": "URL of the remote MCP server"
}
},
"required": [
"type",
"url"
],
"required": ["type", "url"],
"additionalProperties": false
}
]
},
"description": "MCP (Model Context Protocol) server configurations"
},
"experimental": {
"type": "object",
"properties": {
"hook": {
"type": "object",
"properties": {
"file_edited": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"session_completed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5", "name": "opencode",
"version": "0.0.5",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
+12 -9
View File
@@ -142,7 +142,7 @@ if (!snapshot) {
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
"pkgname='${pkg}'",
`pkgver=${version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
@@ -166,14 +166,17 @@ if (!snapshot) {
"",
].join("\n")
await $`rm -rf ./dist/aur-opencode-bin`
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`
for (const pkg of ["opencode", "opencode-bin"]) {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-${pkg}`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
pkgbuild.replace("${pkg}", pkg),
)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
}
// Homebrew formula
const homebrewFormula = [
+27 -22
View File
@@ -13,6 +13,7 @@ export namespace App {
export const Info = z
.object({
user: z.string(),
hostname: z.string(),
git: z.boolean(),
path: z.object({
config: z.string(),
@@ -30,11 +31,21 @@ export namespace App {
})
export type Info = z.infer<typeof Info>
const ctx = Context.create<Awaited<ReturnType<typeof create>>>("app")
const ctx = Context.create<{
info: Info
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
}>("app")
const APP_JSON = "app.json"
async function create(input: { cwd: string }) {
export type Input = {
cwd: string
}
export async function provide<T>(
input: Input,
cb: (app: App.Info) => Promise<T>,
) {
log.info("creating", {
cwd: input.cwd,
})
@@ -62,8 +73,11 @@ export namespace App {
}
>()
const root = git ?? input.cwd
const info: Info = {
user: os.userInfo().username,
hostname: os.hostname(),
time: {
initialized: state.initialized,
},
@@ -72,16 +86,24 @@ export namespace App {
config: Global.Path.config,
state: Global.Path.state,
data,
root: git ?? input.cwd,
root,
cwd: input.cwd,
},
}
const result = {
const app = {
services,
info,
}
return result
return ctx.provide(app, async () => {
const result = await cb(app.info)
for (const [key, entry] of app.services.entries()) {
if (!entry.shutdown) continue
log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state)
}
return result
})
}
export function state<State>(
@@ -107,22 +129,6 @@ export namespace App {
return ctx.use().info
}
export async function provide<T>(
input: { cwd: string },
cb: (app: Info) => Promise<T>,
) {
const app = await create(input)
return ctx.provide(app, async () => {
const result = await cb(app.info)
for (const [key, entry] of app.services.entries()) {
if (!entry.shutdown) continue
log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state)
}
return result
})
}
export async function initialize() {
const { info } = ctx.use()
info.time.initialized = Date.now()
@@ -142,4 +148,3 @@ export namespace App {
.replace(/[^A-Za-z0-9_]/g, "-")
}
}
+5 -6
View File
@@ -3,6 +3,7 @@ import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { NamedError } from "../util/error"
import { readableStreamToText } from "bun"
export namespace BunProc {
const log = Log.create({ service: "bun" })
@@ -25,11 +26,9 @@ export namespace BunProc {
BUN_BE_BUN: "1",
},
})
const code = await result.exited
// @ts-ignore
const stdout = await result.stdout.text()
// @ts-ignore
const stderr = await result.stderr.text()
const code = await result.exited;
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined
log.info("done", {
code,
stdout,
@@ -65,7 +64,7 @@ export namespace BunProc {
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
cwd: Global.Path.cache,
}).catch((e) => {
new InstallFailedError(
throw new InstallFailedError(
{ pkg, version },
{
cause: e,
+4 -2
View File
@@ -49,7 +49,7 @@ export namespace Bus {
)
}
export function publish<Definition extends EventDefinition>(
export async function publish<Definition extends EventDefinition>(
def: Definition,
properties: z.output<Definition["properties"]>,
) {
@@ -60,12 +60,14 @@ export namespace Bus {
log.info("publishing", {
type: def.type,
})
const pending = []
for (const key of [def.type, "*"]) {
const match = state().subscriptions.get(key)
for (const sub of match ?? []) {
sub(payload)
pending.push(sub(payload))
}
}
return Promise.all(pending)
}
export function subscribe<Definition extends EventDefinition>(
+17
View File
@@ -0,0 +1,17 @@
import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { Format } from "../format"
import { Share } from "../share/share"
export async function bootstrap<T>(
input: App.Input,
cb: (app: App.Info) => Promise<T>,
) {
return App.provide(input, async (app) => {
Share.init()
Format.init()
ConfigHooks.init()
return cb(app)
})
}
+8 -8
View File
@@ -31,7 +31,7 @@ export const AuthListCommand = cmd({
UI.empty()
const authPath = path.join(Global.Path.data, "auth.json")
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir)
const displayPath = authPath.startsWith(homedir)
? authPath.replace(homedir, "~")
: authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
@@ -46,14 +46,14 @@ export const AuthListCommand = cmd({
prompts.outro(`${results.length} credentials`)
// Environment variables section
const activeEnvVars: Array<{ provider: string, envVar: string }> = []
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
for (const [providerID, provider] of Object.entries(database)) {
for (const envVar of provider.env) {
if (process.env[envVar]) {
activeEnvVars.push({
provider: provider.name || providerID,
envVar
activeEnvVars.push({
provider: provider.name || providerID,
envVar,
})
}
}
@@ -62,11 +62,11 @@ export const AuthListCommand = cmd({
if (activeEnvVars.length > 0) {
UI.empty()
prompts.intro("Environment")
for (const { provider, envVar } of activeEnvVars) {
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
}
},
+2 -2
View File
@@ -8,7 +8,7 @@ export const ModelsCommand = cmd({
handler: async () => {
await App.provide({ cwd: process.cwd() }, async () => {
const providers = await Provider.list()
for (const [providerID, provider] of Object.entries(providers)) {
for (const modelID of Object.keys(provider.info.models)) {
console.log(`${providerID}/${modelID}`)
@@ -16,4 +16,4 @@ export const ModelsCommand = cmd({
}
})
},
})
})
+94 -96
View File
@@ -1,14 +1,13 @@
import type { Argv } from "yargs"
import { App } from "../../app/app"
import { Bus } from "../../bus"
import { Provider } from "../../provider/provider"
import { Session } from "../../session"
import { Share } from "../../share/share"
import { Message } from "../../session/message"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Flag } from "../../flag/flag"
import { Config } from "../../config/config"
import { bootstrap } from "../bootstrap"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -56,110 +55,109 @@ export const RunCommand = cmd({
},
handler: async (args) => {
const message = args.message.join(" ")
await App.provide(
{
cwd: process.cwd(),
},
async () => {
await Share.init()
const session = await (async () => {
if (args.continue) {
const first = await Session.list().next()
if (first.done) return
return first.value
}
if (args.session) return Session.get(args.session)
return Session.create()
})()
if (!session) {
UI.error("Session not found")
return
await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => {
if (args.continue) {
const first = await Session.list().next()
if (first.done) return
return first.value
}
UI.empty()
UI.println(UI.logo())
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
UI.empty()
if (args.session) return Session.get(args.session)
const cfg = await Config.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id)
UI.println(
UI.Style.TEXT_INFO_BOLD +
return Session.create()
})()
if (!session) {
UI.error("Session not found")
return
}
const isPiped = !process.stdout.isTTY
UI.empty()
UI.println(UI.logo())
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
UI.empty()
const cfg = await Config.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id)
UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://opencode.ai/s/" +
session.id.slice(-8),
)
}
UI.empty()
const { providerID, modelID } = args.model
? Provider.parseModel(args.model)
: await Provider.defaultModel()
UI.println(
UI.Style.TEXT_NORMAL_BOLD + "@ ",
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
)
UI.empty()
}
UI.empty()
function printEvent(color: string, type: string, title: string) {
UI.println(
color + `|`,
UI.Style.TEXT_NORMAL +
UI.Style.TEXT_DIM +
` ${type.padEnd(7, " ")}`,
"",
UI.Style.TEXT_NORMAL + title,
)
const { providerID, modelID } = args.model
? Provider.parseModel(args.model)
: await Provider.defaultModel()
UI.println(
UI.Style.TEXT_NORMAL_BOLD + "@ ",
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
)
UI.empty()
function printEvent(color: string, type: string, title: string) {
UI.println(
color + `|`,
UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
"",
UI.Style.TEXT_NORMAL + title,
)
}
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) return
const part = evt.properties.part
const message = await Session.getMessage(
evt.properties.sessionID,
evt.properties.messageID,
)
if (
part.type === "tool-invocation" &&
part.toolInvocation.state === "result"
) {
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
part.toolInvocation.toolName,
UI.Style.TEXT_INFO_BOLD,
]
printEvent(color, tool, metadata?.title || "Unknown")
}
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) return
const part = evt.properties.part
const message = await Session.getMessage(
evt.properties.sessionID,
evt.properties.messageID,
)
if (
part.type === "tool-invocation" &&
part.toolInvocation.state === "result"
) {
const metadata =
message.metadata.tool[part.toolInvocation.toolCallId]
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
part.toolInvocation.toolName,
UI.Style.TEXT_INFO_BOLD,
]
printEvent(color, tool, metadata?.title || 'Unknown')
if (part.type === "text") {
if (part.text.includes("\n")) {
UI.empty()
UI.println(part.text)
UI.empty()
return
}
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
if (part.type === "text") {
if (part.text.includes("\n")) {
UI.empty()
UI.println(part.text)
UI.empty()
return
}
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
await Session.chat({
sessionID: session.id,
providerID,
modelID,
parts: [
{
type: "text",
text: message,
},
],
})
UI.empty()
},
)
const result = await Session.chat({
sessionID: session.id,
providerID,
modelID,
parts: [
{
type: "text",
text: message,
},
],
})
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
if (match) process.stdout.write(match.text)
}
UI.empty()
})
},
})
+4 -7
View File
@@ -7,12 +7,9 @@ export const ScrapCommand = cmd({
builder: (yargs) =>
yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) {
await App.provide(
{ cwd: process.cwd() },
async () => {
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
},
)
await App.provide({ cwd: process.cwd() }, async () => {
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
})
},
})
+111
View File
@@ -0,0 +1,111 @@
import { Global } from "../../global"
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import { cmd } from "./cmd"
import path from "path"
import fs from "fs/promises"
import { Installation } from "../../installation"
import { Config } from "../../config/config"
import { Bus } from "../../bus"
import { AuthLoginCommand } from "./auth"
export const TuiCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs) =>
yargs.positional("project", {
type: "string",
describe: "path to start opencode in",
}),
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
return
}
const result = await bootstrap({ cwd }, async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
const server = Server.listen({
port: 0,
hostname: "127.0.0.1",
})
let cmd = ["go", "run", "./main.go"]
let cwd = Bun.fileURLToPath(
new URL("../../../../tui/cmd/opencode", import.meta.url),
)
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
let binaryName = blob.name
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
binaryName += ".exe"
}
const binary = path.join(Global.Path.cache, "tui", binaryName)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
},
})
;(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().catch(() => {})
if (!latest) return
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()
return "done"
})
if (result === "done") break
if (result === "needs_provider") {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
await AuthLoginCommand.handler(args)
UI.empty()
UI.println("Provider configured - please run again")
return
}
}
},
})
+27
View File
@@ -22,6 +22,7 @@ export namespace Config {
}
}
log.info("loaded", result)
return result
})
@@ -167,6 +168,32 @@ export namespace Config {
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
experimental: z
.object({
hook: z
.object({
file_edited: z
.record(
z.string(),
z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array(),
)
.optional(),
session_completed: z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array()
.optional(),
})
.optional(),
})
.optional(),
})
.strict()
.openapi({
+54
View File
@@ -0,0 +1,54 @@
import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { Session } from "../session"
import { Log } from "../util/log"
import { Config } from "./config"
import path from "path"
export namespace ConfigHooks {
const log = Log.create({ service: "config.hooks" })
export function init() {
log.info("init")
const app = App.info()
Bus.subscribe(File.Event.Edited, async (payload) => {
const cfg = await Config.get()
const ext = path.extname(payload.properties.file)
for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) {
log.info("file_edited", {
file: payload.properties.file,
command: item.command,
})
Bun.spawn({
cmd: item.command.map((x) =>
x.replace("$FILE", payload.properties.file),
),
env: item.environment,
cwd: app.path.cwd,
stdout: "ignore",
stderr: "ignore",
})
}
})
Bus.subscribe(Session.Event.Idle, async () => {
const cfg = await Config.get()
if (cfg.experimental?.hook?.session_completed) {
for (const item of cfg.experimental.hook.session_completed) {
log.info("session_completed", {
command: item.command,
})
Bun.spawn({
cmd: item.command,
cwd: App.info().path.cwd,
env: item.environment,
stdout: "ignore",
stderr: "ignore",
})
}
}
})
}
}
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod"
import { Bus } from "../bus"
export namespace File {
export const Event = {
Edited: Bus.event(
"file.edited",
z.object({
file: z.string(),
}),
),
}
}
@@ -1,6 +1,6 @@
import { App } from "../../app/app"
import { App } from "../app/app"
export namespace FileTimes {
export namespace FileTime {
export const state = App.state("tool.filetimes", () => {
const read: {
[sessionID: string]: {
+165
View File
@@ -0,0 +1,165 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
import { Bus } from "../bus"
import { File } from "../file"
import { Log } from "../util/log"
import path from "path"
export namespace Format {
const log = Log.create({ service: "format" })
const state = App.state("format", () => {
const enabled: Record<string, boolean> = {}
return {
enabled,
}
})
async function isEnabled(item: Definition) {
const s = state()
let status = s.enabled[item.name]
if (status === undefined) {
status = await item.enabled()
s.enabled[item.name] = status
}
return status
}
async function getFormatter(ext: string) {
const result = []
for (const item of FORMATTERS) {
if (!item.extensions.includes(ext)) continue
if (!isEnabled(item)) continue
result.push(item)
}
return result
}
export function init() {
log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)
for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd,
env: item.environment,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
}
})
}
interface Definition {
name: string
command: string[]
environment?: Record<string, string>
extensions: string[]
enabled(): Promise<boolean>
}
const FORMATTERS: Definition[] = [
{
name: "prettier",
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".vue",
".svelte",
".json",
".jsonc",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".graphql",
".gql",
],
async enabled() {
try {
const proc = Bun.spawn({
cmd: [BunProc.which(), "run", "prettier", "--version"],
cwd: App.info().path.cwd,
env: {
BUN_BE_BUN: "1",
},
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
{
name: "mix",
command: ["mix", "format", "$FILE"],
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
async enabled() {
try {
const proc = Bun.spawn({
cmd: ["mix", "--version"],
cwd: App.info().path.cwd,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
{
name: "gofmt",
command: ["gofmt", "-w", "$FILE"],
extensions: [".go"],
async enabled() {
try {
const proc = Bun.spawn({
cmd: ["gofmt", "-h"],
cwd: App.info().path.cwd,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
]
}
+17 -109
View File
@@ -1,28 +1,19 @@
import "zod-openapi/extend"
import { App } from "./app/app"
import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
import { Share } from "./share/share"
import url from "node:url"
import { Global } from "./global"
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"
import { GenerateCommand } from "./cli/cmd/generate"
import { ScrapCommand } from "./cli/cmd/scrap"
import { Log } from "./util/log"
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
import { AuthCommand } from "./cli/cmd/auth"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { Provider } from "./provider/provider"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
import { Bus } from "./bus"
import { Config } from "./config/config"
import { NamedError } from "./util/error"
import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"
import { TuiCommand } from "./cli/cmd/tui"
const cancel = new AbortController()
@@ -55,103 +46,7 @@ const cli = yargs(hideBin(process.argv))
})
})
.usage("\n" + UI.logo())
.command({
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs) =>
yargs.positional("project", {
type: "string",
describe: "path to start opencode in",
}),
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
return
}
const result = await App.provide({ cwd }, async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
await Share.init()
const server = Server.listen({
port: 0,
hostname: "127.0.0.1",
})
let cmd = ["go", "run", "./main.go"]
let cwd = url.fileURLToPath(
new URL("../../tui/cmd/opencode", import.meta.url),
)
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
let binaryName = blob.name
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
binaryName += ".exe"
}
const binary = path.join(Global.Path.cache, "tui", binaryName)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
signal: cancel.signal,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
},
})
;(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().catch(() => {})
if (!latest) return
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()
return "done"
})
if (result === "done") break
if (result === "needs_provider") {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
await AuthLoginCommand.handler(args)
}
}
},
})
.command(TuiCommand)
.command(RunCommand)
.command(GenerateCommand)
.command(ScrapCommand)
@@ -172,19 +67,32 @@ const cli = yargs(hideBin(process.argv))
try {
await cli.parse()
} catch (e) {
const data: Record<string, any> = {}
let data: Record<string, any> = {}
if (e instanceof NamedError) {
const obj = e.toObject()
Object.assign(data, {
...obj.data,
})
}
if (e instanceof Error) {
Object.assign(data, {
name: e.name,
message: e.message,
cause: e.cause?.toString(),
})
}
if (e instanceof ResolveMessage) {
Object.assign(data, {
name: e.name,
message: e.message,
code: e.code,
specifier: e.specifier,
referrer: e.referrer,
position: e.position,
importKind: e.importKind,
});
}
Log.Default.error("fatal", data)
const formatted = FormatError(e)
+1 -2
View File
@@ -23,7 +23,6 @@ import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -411,7 +410,7 @@ export namespace Provider {
// MultiEditTool,
WriteTool,
TodoWriteTool,
TaskTool,
// TaskTool,
TodoReadTool,
]
+10
View File
@@ -78,6 +78,12 @@ export namespace Session {
info: Info,
}),
),
Idle: Bus.event(
"session.idle",
z.object({
sessionID: z.string(),
}),
),
Error: Bus.event(
"session.error",
z.object({
@@ -537,6 +543,7 @@ export namespace Session {
// return step
// },
toolCallStreaming: true,
maxTokens: model.info.limit.output || undefined,
abortSignal: abort.signal,
maxSteps: 1000,
providerOptions: model.info.options,
@@ -853,6 +860,9 @@ export namespace Session {
[Symbol.dispose]() {
log.info("unlocking", { sessionID })
state().pending.delete(sessionID)
Bus.publish(Event.Idle, {
sessionID,
})
},
}
}
+4 -9
View File
@@ -1,4 +1,3 @@
import { App } from "../app/app"
import { Bus } from "../bus"
import { Installation } from "../installation"
import { Session } from "../session"
@@ -11,12 +10,6 @@ export namespace Share {
let queue: Promise<void> = Promise.resolve()
const pending = new Map<string, any>()
const state = App.state("share", async () => {
Bus.subscribe(Storage.Event.Write, async (payload) => {
await sync(payload.properties.key, payload.properties.content)
})
})
export async function sync(key: string, content: any) {
const [root, ...splits] = key.split("/")
if (root !== "session") return
@@ -52,8 +45,10 @@ export namespace Share {
})
}
export async function init() {
await state()
export function init() {
Bus.subscribe(Storage.Event.Write, async (payload) => {
await sync(payload.properties.key, payload.properties.content)
})
}
export const URL =
+13 -4
View File
@@ -5,12 +5,14 @@
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff"
import { Permission } from "../permission"
import DESCRIPTION from "./edit.txt"
import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
export const EditTool = Tool.define({
id: "edit",
@@ -59,6 +61,9 @@ export const EditTool = Tool.define({
if (params.oldString === "") {
contentNew = params.newString
await Bun.write(filepath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
return
}
@@ -67,7 +72,7 @@ export const EditTool = Tool.define({
if (!stats) throw new Error(`File ${filepath} not found`)
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${filepath}`)
await FileTimes.assert(ctx.sessionID, filepath)
await FileTime.assert(ctx.sessionID, filepath)
contentOld = await file.text()
contentNew = replace(
@@ -77,13 +82,17 @@ export const EditTool = Tool.define({
params.replaceAll,
)
await file.write(contentNew)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
contentNew = await file.text()
})()
const diff = trimDiff(
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
)
FileTimes.read(ctx.sessionID, filepath)
FileTime.read(ctx.sessionID, filepath)
let output = ""
await LSP.touchFile(filepath, true)
@@ -473,7 +482,7 @@ export function replace(
if (oldString === newString) {
throw new Error("oldString and newString must be different")
}
for (const replacer of [
SimpleReplacer,
LineTrimmedReplacer,
+3 -3
View File
@@ -2,7 +2,7 @@ import { z } from "zod"
import * as path from "path"
import * as fs from "fs/promises"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { FileTime } from "../file/time"
import DESCRIPTION from "./patch.txt"
const PatchParams = z.object({
@@ -244,7 +244,7 @@ export const PatchTool = Tool.define({
absPath = path.resolve(process.cwd(), absPath)
}
await FileTimes.assert(ctx.sessionID, absPath)
await FileTime.assert(ctx.sessionID, absPath)
try {
const stats = await fs.stat(absPath)
@@ -351,7 +351,7 @@ export const PatchTool = Tool.define({
totalAdditions += additions
totalRemovals += removals
FileTimes.read(ctx.sessionID, absPath)
FileTime.read(ctx.sessionID, absPath)
}
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
+2 -2
View File
@@ -3,7 +3,7 @@ import * as fs from "fs"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
import { FileTimes } from "./util/file-times"
import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt"
import { App } from "../app/app"
@@ -90,7 +90,7 @@ export const ReadTool = Tool.define({
// just warms the lsp client
await LSP.touchFile(filePath, true)
FileTimes.read(ctx.sessionID, filePath)
FileTime.read(ctx.sessionID, filePath)
return {
output,
+8 -3
View File
@@ -1,11 +1,13 @@
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"
import { Permission } from "../permission"
import DESCRIPTION from "./write.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
export const WriteTool = Tool.define({
id: "write",
@@ -26,7 +28,7 @@ export const WriteTool = Tool.define({
const file = Bun.file(filepath)
const exists = await file.exists()
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
if (exists) await FileTime.assert(ctx.sessionID, filepath)
await Permission.ask({
id: "write",
@@ -42,7 +44,10 @@ export const WriteTool = Tool.define({
})
await Bun.write(filepath, params.content)
FileTimes.read(ctx.sessionID, filepath)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
FileTime.read(ctx.sessionID, filepath)
let output = ""
await LSP.touchFile(filepath, true)
-1
View File
@@ -8,4 +8,3 @@ export function lazy<T>(fn: () => T) {
return value as T
}
}
+4 -1
View File
@@ -19,7 +19,10 @@ export namespace Log {
await fs.mkdir(dir, { recursive: true })
cleanup(dir)
if (options.print) return
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
logpath = path.join(
dir,
new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const logfile = Bun.file(logpath)
await fs.truncate(logpath).catch(() => {})
const writer = logfile.writer()
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
export {}
+4 -4
View File
@@ -316,13 +316,13 @@ const testCases: TestCase[] = [
// WhitespaceNormalizedReplacer - test regex special characters that could cause errors
{
content: 'const pattern = "test[123]";',
find: 'test[123]',
replace: 'test[456]',
find: "test[123]",
replace: "test[456]",
},
{
content: 'const regex = "^start.*end$";',
find: '^start.*end$',
replace: '^begin.*finish$',
find: "^start.*end$",
replace: "^begin.*finish$",
},
// EscapeNormalizedReplacer - test single backslash vs double backslash
+26
View File
@@ -0,0 +1,26 @@
# TUI Agent Guidelines
## Build/Test Commands
- **Build**: `go build ./cmd/opencode` (builds main binary)
- **Test**: `go test ./...` (runs all tests)
- **Single test**: `go test ./internal/theme -run TestLoadThemesFromJSON` (specific test)
- **Generate client**: `go generate ./pkg/client/` (after server endpoint changes)
- **Release build**: Uses `.goreleaser.yml` configuration
## Code Style
- **Language**: Go 1.24+ with standard formatting (`gofmt`)
- **Imports**: Group standard, third-party, local packages with blank lines
- **Naming**: Go conventions - PascalCase exports, camelCase private, ALL_CAPS constants
- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
- **Structs**: Define clear interfaces, embed when appropriate
- **Testing**: Use table-driven tests, `t.TempDir()` for file operations
## Architecture
- **TUI Framework**: Bubble Tea v2 with Lipgloss v2 for styling
- **Client**: Generated OpenAPI client communicates with TypeScript server
- **Components**: Reusable UI components in `internal/components/`
- **Themes**: JSON-based theming system with override hierarchy
- **State**: Centralized app state with message passing
+2 -1
View File
@@ -49,6 +49,8 @@ func main() {
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
slog.SetDefault(logger)
slog.Debug("TUI launched", "app", appInfo)
httpClient, err := client.NewClientWithResponses(url)
if err != nil {
slog.Error("Failed to create client", "error", err)
@@ -66,7 +68,6 @@ func main() {
program := tea.NewProgram(
tui.NewModel(app_),
// tea.WithColorProfile(colorprofile.ANSI),
tea.WithAltScreen(),
tea.WithKeyboardEnhancements(),
tea.WithMouseCellMotion(),
@@ -83,7 +83,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
} else {
existingValue := m.textarea.Value()
// Replace the current token (after last space)
lastSpaceIndex := strings.LastIndex(existingValue, " ")
if lastSpaceIndex == -1 {
@@ -124,6 +124,11 @@ func (m *editorComponent) Content() string {
Width(m.width).
PaddingTop(1).
PaddingBottom(1).
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(t.Border()).
BorderBackground(t.Background()).
BorderLeft(true).
BorderRight(true).
Render(textarea)
hint := base(m.getSubmitKeyText()) + muted(" send ")
@@ -177,8 +182,6 @@ func (m *editorComponent) GetSize() (width, height int) {
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
m.width = width
m.height = height
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
// m.textarea.SetHeight(height - 4)
return nil
}
@@ -324,10 +327,11 @@ func createTextArea(existing *textarea.Model) textarea.Model {
ta.Prompt = " "
ta.ShowLineNumbers = false
ta.CharLimit = -1
ta.SetWidth(layout.Current.Container.Width - 6)
if existing != nil {
ta.SetValue(existing.Value())
ta.SetWidth(existing.Width())
// ta.SetWidth(existing.Width())
ta.SetHeight(existing.Height())
}
@@ -341,7 +345,7 @@ func createSpinner() spinner.Model {
spinner.WithSpinner(spinner.Ellipsis),
spinner.WithStyle(
styles.NewStyle().
Foreground(t.Background()).
Background(t.Background()).
Foreground(t.TextMuted()).
Width(3).
Lipgloss(),
@@ -226,11 +226,18 @@ func renderText(message client.MessageInfo, text string, author string) string {
if message.Role == client.Assistant {
markdownWidth = width - padding - 4 - 3
}
if message.Role == client.User {
text = strings.ReplaceAll(text, "<", "\\<")
text = strings.ReplaceAll(text, ">", "\\>")
minWidth := max(markdownWidth, (width-4)/2)
messageStyle := styles.NewStyle().
Width(minWidth).
Background(t.BackgroundPanel()).
Foreground(t.Text())
if textWidth < minWidth {
messageStyle = messageStyle.AlignHorizontal(lipgloss.Right)
}
content := messageStyle.Render(text)
if message.Role == client.Assistant {
content = toMarkdown(text, markdownWidth, t.BackgroundPanel())
}
content := toMarkdown(text, markdownWidth, t.BackgroundPanel())
content = strings.Join([]string{content, info}, "\n")
switch message.Role {
@@ -397,6 +404,11 @@ func renderToolInvocation(
body,
styles.WhitespaceStyle(t.Background()),
)
// Add diagnostics at the bottom if they exist
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
}
}
}
case "write":
@@ -404,6 +416,11 @@ func renderToolInvocation(
title = fmt.Sprintf("WRITE %s", relative(filename))
if content, ok := toolArgsMap["content"].(string); ok {
body = renderFile(filename, content)
// Add diagnostics at the bottom if they exist
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
}
}
}
case "bash":
@@ -685,3 +702,81 @@ func extension(path string) string {
}
return ext
}
// Diagnostic represents an LSP diagnostic
type Diagnostic struct {
Range struct {
Start struct {
Line int `json:"line"`
Character int `json:"character"`
} `json:"start"`
} `json:"range"`
Severity int `json:"severity"`
Message string `json:"message"`
}
// renderDiagnostics formats LSP diagnostics for display in the TUI
func renderDiagnostics(metadata client.MessageMetadata_Tool_AdditionalProperties, filePath string) string {
diagnosticsData, ok := metadata.Get("diagnostics")
if !ok {
return ""
}
// diagnosticsData should be a map[string][]Diagnostic
diagnosticsMap, ok := diagnosticsData.(map[string]interface{})
if !ok {
return ""
}
fileDiagnostics, ok := diagnosticsMap[filePath]
if !ok {
return ""
}
diagnosticsList, ok := fileDiagnostics.([]interface{})
if !ok {
return ""
}
var errorDiagnostics []string
for _, diagInterface := range diagnosticsList {
diagMap, ok := diagInterface.(map[string]interface{})
if !ok {
continue
}
// Parse the diagnostic
var diag Diagnostic
diagBytes, err := json.Marshal(diagMap)
if err != nil {
continue
}
if err := json.Unmarshal(diagBytes, &diag); err != nil {
continue
}
// Only show error diagnostics (severity === 1)
if diag.Severity != 1 {
continue
}
line := diag.Range.Start.Line + 1 // 1-based
column := diag.Range.Start.Character + 1 // 1-based
errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
}
if len(errorDiagnostics) == 0 {
return ""
}
t := theme.CurrentTheme()
var result strings.Builder
for _, diagnostic := range errorDiagnostics {
if result.Len() > 0 {
result.WriteString("\n")
}
result.WriteString(styles.NewStyle().Foreground(t.Error()).Render(diagnostic))
}
return result.String()
}
@@ -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"
@@ -34,8 +35,7 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
Padding(0, 1)
if selected {
itemStyle = itemStyle.
Foreground(t.Primary())
itemStyle = itemStyle.Foreground(t.Primary())
}
title := itemStyle.Render(
@@ -199,8 +199,14 @@ func (c *completionDialogComponent) View() string {
c.list.SetMaxWidth(maxWidth)
return baseStyle.Padding(0, 0).
return baseStyle.
Padding(0, 0).
Background(t.BackgroundElement()).
BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(true).
BorderForeground(t.Border()).
BorderBackground(t.Background()).
Width(c.width).
Render(c.list.View())
}
@@ -49,14 +49,14 @@ func (s sessionItem) Render(selected bool, width int) string {
// Red background for delete confirmation
itemStyle = baseStyle.
Background(t.Error()).
Foreground(t.Background()).
Foreground(t.BackgroundElement()).
Width(width).
PaddingLeft(1)
} else {
// Normal selection
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.Background()).
Foreground(t.BackgroundElement()).
Width(width).
PaddingLeft(1)
}
@@ -181,7 +181,7 @@ func (s StringItem) Render(selected bool, width int) string {
if selected {
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.Background()).
Foreground(t.BackgroundElement()).
Width(width).
PaddingLeft(1)
} else {
+1 -1
View File
@@ -18,7 +18,7 @@ type State struct {
func NewState() *State {
return &State{
Theme: "system",
Theme: "opencode",
}
}
+18 -12
View File
@@ -21,7 +21,7 @@ func TestLoadThemesFromJSON(t *testing.T) {
}
// Check for expected themes
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu"}
for _, expected := range expectedThemes {
found := slices.Contains(themes, expected)
if !found {
@@ -43,22 +43,28 @@ func TestLoadThemesFromJSON(t *testing.T) {
}
func TestColorReferenceResolution(t *testing.T) {
// Test the example theme which uses references
example := GetTheme("example")
if example == nil {
t.Fatal("Failed to get example theme")
// Load themes first
err := LoadThemesFromJSON()
if err != nil {
t.Fatalf("Failed to load themes: %v", err)
}
// Check that brandBlue reference was resolved
primary := example.Primary()
// Test a theme that uses references (e.g., solarized uses color definitions)
solarized := GetTheme("solarized")
if solarized == nil {
t.Fatal("Failed to get solarized theme")
}
// Check that color references were resolved
primary := solarized.Primary()
if primary.Dark == nil || primary.Light == nil {
t.Error("Primary color (brandBlue reference) not resolved")
t.Error("Primary color reference not resolved")
}
// Check that nested reference (borderActive -> primary -> brandBlue) works
borderActive := example.BorderActive()
if borderActive.Dark == nil || borderActive.Light == nil {
t.Error("BorderActive color (nested reference) not resolved")
// Check that all colors are properly resolved
text := solarized.Text()
if text.Dark == nil || text.Light == nil {
t.Error("Text color reference not resolved")
}
}
+5 -5
View File
@@ -93,16 +93,16 @@ func AvailableThemes() []string {
names = append(names, name)
}
slices.SortFunc(names, func(a, b string) int {
if a == "system" {
return -1
} else if b == "system" {
return 1
}
if a == "opencode" {
return -1
} else if b == "opencode" {
return 1
}
if a == "system" {
return -1
} else if b == "system" {
return 1
}
return strings.Compare(a, b)
})
return names
@@ -78,4 +78,3 @@
"syntaxPunctuation": "darkFg"
}
}
@@ -110,4 +110,3 @@
"syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
}
}
@@ -0,0 +1,228 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background": "#193549",
"backgroundAlt": "#122738",
"backgroundPanel": "#1f4662",
"foreground": "#ffffff",
"foregroundMuted": "#adb7c9",
"yellow": "#ffc600",
"yellowBright": "#ffe14c",
"orange": "#ff9d00",
"orangeBright": "#ffb454",
"mint": "#2affdf",
"mintBright": "#7efff5",
"blue": "#0088ff",
"blueBright": "#5cb7ff",
"pink": "#ff628c",
"pinkBright": "#ff86a5",
"green": "#9eff80",
"greenBright": "#b9ff9f",
"purple": "#9a5feb",
"purpleBright": "#b88cfd",
"red": "#ff0088",
"redBright": "#ff5fb3"
},
"theme": {
"primary": {
"dark": "blue",
"light": "#0066cc"
},
"secondary": {
"dark": "purple",
"light": "#7c4dff"
},
"accent": {
"dark": "mint",
"light": "#00acc1"
},
"error": {
"dark": "red",
"light": "#e91e63"
},
"warning": {
"dark": "yellow",
"light": "#ff9800"
},
"success": {
"dark": "green",
"light": "#4caf50"
},
"info": {
"dark": "orange",
"light": "#ff5722"
},
"text": {
"dark": "foreground",
"light": "#193549"
},
"textMuted": {
"dark": "foregroundMuted",
"light": "#5c6b7d"
},
"background": {
"dark": "#193549",
"light": "#ffffff"
},
"backgroundPanel": {
"dark": "#122738",
"light": "#f5f7fa"
},
"backgroundElement": {
"dark": "#1f4662",
"light": "#e8ecf1"
},
"border": {
"dark": "#1f4662",
"light": "#d3dae3"
},
"borderActive": {
"dark": "blue",
"light": "#0066cc"
},
"borderSubtle": {
"dark": "#0e1e2e",
"light": "#e8ecf1"
},
"diffAdded": {
"dark": "green",
"light": "#4caf50"
},
"diffRemoved": {
"dark": "red",
"light": "#e91e63"
},
"diffContext": {
"dark": "foregroundMuted",
"light": "#5c6b7d"
},
"diffHunkHeader": {
"dark": "mint",
"light": "#00acc1"
},
"diffHighlightAdded": {
"dark": "greenBright",
"light": "#4caf50"
},
"diffHighlightRemoved": {
"dark": "redBright",
"light": "#e91e63"
},
"diffAddedBg": {
"dark": "#1a3a2a",
"light": "#e8f5e9"
},
"diffRemovedBg": {
"dark": "#3a1a2a",
"light": "#ffebee"
},
"diffContextBg": {
"dark": "#122738",
"light": "#f5f7fa"
},
"diffLineNumber": {
"dark": "#2d5a7b",
"light": "#b0bec5"
},
"diffAddedLineNumberBg": {
"dark": "#1a3a2a",
"light": "#e8f5e9"
},
"diffRemovedLineNumberBg": {
"dark": "#3a1a2a",
"light": "#ffebee"
},
"markdownText": {
"dark": "foreground",
"light": "#193549"
},
"markdownHeading": {
"dark": "yellow",
"light": "#ff9800"
},
"markdownLink": {
"dark": "blue",
"light": "#0066cc"
},
"markdownLinkText": {
"dark": "mint",
"light": "#00acc1"
},
"markdownCode": {
"dark": "green",
"light": "#4caf50"
},
"markdownBlockQuote": {
"dark": "foregroundMuted",
"light": "#5c6b7d"
},
"markdownEmph": {
"dark": "orange",
"light": "#ff5722"
},
"markdownStrong": {
"dark": "pink",
"light": "#e91e63"
},
"markdownHorizontalRule": {
"dark": "#2d5a7b",
"light": "#d3dae3"
},
"markdownListItem": {
"dark": "blue",
"light": "#0066cc"
},
"markdownListEnumeration": {
"dark": "mint",
"light": "#00acc1"
},
"markdownImage": {
"dark": "blue",
"light": "#0066cc"
},
"markdownImageText": {
"dark": "mint",
"light": "#00acc1"
},
"markdownCodeBlock": {
"dark": "foreground",
"light": "#193549"
},
"syntaxComment": {
"dark": "#0088ff",
"light": "#5c6b7d"
},
"syntaxKeyword": {
"dark": "orange",
"light": "#ff5722"
},
"syntaxFunction": {
"dark": "yellow",
"light": "#ff9800"
},
"syntaxVariable": {
"dark": "foreground",
"light": "#193549"
},
"syntaxString": {
"dark": "green",
"light": "#4caf50"
},
"syntaxNumber": {
"dark": "pink",
"light": "#e91e63"
},
"syntaxType": {
"dark": "mint",
"light": "#00acc1"
},
"syntaxOperator": {
"dark": "orange",
"light": "#ff5722"
},
"syntaxPunctuation": {
"dark": "foreground",
"light": "#193549"
}
}
}
@@ -0,0 +1,219 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background": "#282a36",
"currentLine": "#44475a",
"selection": "#44475a",
"foreground": "#f8f8f2",
"comment": "#6272a4",
"cyan": "#8be9fd",
"green": "#50fa7b",
"orange": "#ffb86c",
"pink": "#ff79c6",
"purple": "#bd93f9",
"red": "#ff5555",
"yellow": "#f1fa8c"
},
"theme": {
"primary": {
"dark": "purple",
"light": "purple"
},
"secondary": {
"dark": "pink",
"light": "pink"
},
"accent": {
"dark": "cyan",
"light": "cyan"
},
"error": {
"dark": "red",
"light": "red"
},
"warning": {
"dark": "yellow",
"light": "yellow"
},
"success": {
"dark": "green",
"light": "green"
},
"info": {
"dark": "orange",
"light": "orange"
},
"text": {
"dark": "foreground",
"light": "#282a36"
},
"textMuted": {
"dark": "comment",
"light": "#6272a4"
},
"background": {
"dark": "#282a36",
"light": "#f8f8f2"
},
"backgroundPanel": {
"dark": "#21222c",
"light": "#e8e8e2"
},
"backgroundElement": {
"dark": "currentLine",
"light": "#d8d8d2"
},
"border": {
"dark": "currentLine",
"light": "#c8c8c2"
},
"borderActive": {
"dark": "purple",
"light": "purple"
},
"borderSubtle": {
"dark": "#191a21",
"light": "#e0e0e0"
},
"diffAdded": {
"dark": "green",
"light": "green"
},
"diffRemoved": {
"dark": "red",
"light": "red"
},
"diffContext": {
"dark": "comment",
"light": "#6272a4"
},
"diffHunkHeader": {
"dark": "comment",
"light": "#6272a4"
},
"diffHighlightAdded": {
"dark": "green",
"light": "green"
},
"diffHighlightRemoved": {
"dark": "red",
"light": "red"
},
"diffAddedBg": {
"dark": "#1a3a1a",
"light": "#e0ffe0"
},
"diffRemovedBg": {
"dark": "#3a1a1a",
"light": "#ffe0e0"
},
"diffContextBg": {
"dark": "#21222c",
"light": "#e8e8e2"
},
"diffLineNumber": {
"dark": "currentLine",
"light": "#c8c8c2"
},
"diffAddedLineNumberBg": {
"dark": "#1a3a1a",
"light": "#e0ffe0"
},
"diffRemovedLineNumberBg": {
"dark": "#3a1a1a",
"light": "#ffe0e0"
},
"markdownText": {
"dark": "foreground",
"light": "#282a36"
},
"markdownHeading": {
"dark": "purple",
"light": "purple"
},
"markdownLink": {
"dark": "cyan",
"light": "cyan"
},
"markdownLinkText": {
"dark": "pink",
"light": "pink"
},
"markdownCode": {
"dark": "green",
"light": "green"
},
"markdownBlockQuote": {
"dark": "comment",
"light": "#6272a4"
},
"markdownEmph": {
"dark": "yellow",
"light": "yellow"
},
"markdownStrong": {
"dark": "orange",
"light": "orange"
},
"markdownHorizontalRule": {
"dark": "comment",
"light": "#6272a4"
},
"markdownListItem": {
"dark": "purple",
"light": "purple"
},
"markdownListEnumeration": {
"dark": "cyan",
"light": "cyan"
},
"markdownImage": {
"dark": "cyan",
"light": "cyan"
},
"markdownImageText": {
"dark": "pink",
"light": "pink"
},
"markdownCodeBlock": {
"dark": "foreground",
"light": "#282a36"
},
"syntaxComment": {
"dark": "comment",
"light": "#6272a4"
},
"syntaxKeyword": {
"dark": "pink",
"light": "pink"
},
"syntaxFunction": {
"dark": "green",
"light": "green"
},
"syntaxVariable": {
"dark": "foreground",
"light": "#282a36"
},
"syntaxString": {
"dark": "yellow",
"light": "yellow"
},
"syntaxNumber": {
"dark": "purple",
"light": "purple"
},
"syntaxType": {
"dark": "cyan",
"light": "cyan"
},
"syntaxOperator": {
"dark": "pink",
"light": "pink"
},
"syntaxPunctuation": {
"dark": "foreground",
"light": "#282a36"
}
}
}
@@ -239,4 +239,3 @@
}
}
}
@@ -0,0 +1,233 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#0d1117",
"darkBgAlt": "#010409",
"darkBgPanel": "#161b22",
"darkFg": "#c9d1d9",
"darkFgMuted": "#8b949e",
"darkBlue": "#58a6ff",
"darkGreen": "#3fb950",
"darkRed": "#f85149",
"darkOrange": "#d29922",
"darkPurple": "#bc8cff",
"darkPink": "#ff7b72",
"darkYellow": "#e3b341",
"darkCyan": "#39c5cf",
"lightBg": "#ffffff",
"lightBgAlt": "#f6f8fa",
"lightBgPanel": "#f0f3f6",
"lightFg": "#24292f",
"lightFgMuted": "#57606a",
"lightBlue": "#0969da",
"lightGreen": "#1a7f37",
"lightRed": "#cf222e",
"lightOrange": "#bc4c00",
"lightPurple": "#8250df",
"lightPink": "#bf3989",
"lightYellow": "#9a6700",
"lightCyan": "#1b7c83"
},
"theme": {
"primary": {
"dark": "darkBlue",
"light": "lightBlue"
},
"secondary": {
"dark": "darkPurple",
"light": "lightPurple"
},
"accent": {
"dark": "darkCyan",
"light": "lightCyan"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkYellow",
"light": "lightYellow"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkOrange",
"light": "lightOrange"
},
"text": {
"dark": "darkFg",
"light": "lightFg"
},
"textMuted": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"background": {
"dark": "darkBg",
"light": "lightBg"
},
"backgroundPanel": {
"dark": "darkBgAlt",
"light": "lightBgAlt"
},
"backgroundElement": {
"dark": "darkBgPanel",
"light": "lightBgPanel"
},
"border": {
"dark": "#30363d",
"light": "#d0d7de"
},
"borderActive": {
"dark": "darkBlue",
"light": "lightBlue"
},
"borderSubtle": {
"dark": "#21262d",
"light": "#d8dee4"
},
"diffAdded": {
"dark": "darkGreen",
"light": "lightGreen"
},
"diffRemoved": {
"dark": "darkRed",
"light": "lightRed"
},
"diffContext": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"diffHunkHeader": {
"dark": "darkBlue",
"light": "lightBlue"
},
"diffHighlightAdded": {
"dark": "#3fb950",
"light": "#1a7f37"
},
"diffHighlightRemoved": {
"dark": "#f85149",
"light": "#cf222e"
},
"diffAddedBg": {
"dark": "#033a16",
"light": "#dafbe1"
},
"diffRemovedBg": {
"dark": "#67060c",
"light": "#ffebe9"
},
"diffContextBg": {
"dark": "darkBgAlt",
"light": "lightBgAlt"
},
"diffLineNumber": {
"dark": "#484f58",
"light": "#afb8c1"
},
"diffAddedLineNumberBg": {
"dark": "#033a16",
"light": "#dafbe1"
},
"diffRemovedLineNumberBg": {
"dark": "#67060c",
"light": "#ffebe9"
},
"markdownText": {
"dark": "darkFg",
"light": "lightFg"
},
"markdownHeading": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownLink": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkPink",
"light": "lightPink"
},
"markdownBlockQuote": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "#30363d",
"light": "#d0d7de"
},
"markdownListItem": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxComment": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"syntaxKeyword": {
"dark": "darkPink",
"light": "lightRed"
},
"syntaxFunction": {
"dark": "darkPurple",
"light": "lightPurple"
},
"syntaxVariable": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxString": {
"dark": "darkCyan",
"light": "lightBlue"
},
"syntaxNumber": {
"dark": "darkBlue",
"light": "lightCyan"
},
"syntaxType": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxOperator": {
"dark": "darkPink",
"light": "lightRed"
},
"syntaxPunctuation": {
"dark": "darkFg",
"light": "lightFg"
}
}
}
@@ -0,0 +1,235 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#263238",
"darkBgAlt": "#1e272c",
"darkBgPanel": "#37474f",
"darkFg": "#eeffff",
"darkFgMuted": "#546e7a",
"darkRed": "#f07178",
"darkPink": "#f78c6c",
"darkOrange": "#ffcb6b",
"darkYellow": "#ffcb6b",
"darkGreen": "#c3e88d",
"darkCyan": "#89ddff",
"darkBlue": "#82aaff",
"darkPurple": "#c792ea",
"darkViolet": "#bb80b3",
"lightBg": "#fafafa",
"lightBgAlt": "#f5f5f5",
"lightBgPanel": "#e7e7e8",
"lightFg": "#263238",
"lightFgMuted": "#90a4ae",
"lightRed": "#e53935",
"lightPink": "#ec407a",
"lightOrange": "#f4511e",
"lightYellow": "#ffb300",
"lightGreen": "#91b859",
"lightCyan": "#39adb5",
"lightBlue": "#6182b8",
"lightPurple": "#7c4dff",
"lightViolet": "#945eb8"
},
"theme": {
"primary": {
"dark": "darkBlue",
"light": "lightBlue"
},
"secondary": {
"dark": "darkPurple",
"light": "lightPurple"
},
"accent": {
"dark": "darkCyan",
"light": "lightCyan"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkYellow",
"light": "lightYellow"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkOrange",
"light": "lightOrange"
},
"text": {
"dark": "darkFg",
"light": "lightFg"
},
"textMuted": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"background": {
"dark": "darkBg",
"light": "lightBg"
},
"backgroundPanel": {
"dark": "darkBgAlt",
"light": "lightBgAlt"
},
"backgroundElement": {
"dark": "darkBgPanel",
"light": "lightBgPanel"
},
"border": {
"dark": "#37474f",
"light": "#e0e0e0"
},
"borderActive": {
"dark": "darkBlue",
"light": "lightBlue"
},
"borderSubtle": {
"dark": "#1e272c",
"light": "#eeeeee"
},
"diffAdded": {
"dark": "darkGreen",
"light": "lightGreen"
},
"diffRemoved": {
"dark": "darkRed",
"light": "lightRed"
},
"diffContext": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"diffHunkHeader": {
"dark": "darkCyan",
"light": "lightCyan"
},
"diffHighlightAdded": {
"dark": "darkGreen",
"light": "lightGreen"
},
"diffHighlightRemoved": {
"dark": "darkRed",
"light": "lightRed"
},
"diffAddedBg": {
"dark": "#2e3c2b",
"light": "#e8f5e9"
},
"diffRemovedBg": {
"dark": "#3c2b2b",
"light": "#ffebee"
},
"diffContextBg": {
"dark": "darkBgAlt",
"light": "lightBgAlt"
},
"diffLineNumber": {
"dark": "#37474f",
"light": "#cfd8dc"
},
"diffAddedLineNumberBg": {
"dark": "#2e3c2b",
"light": "#e8f5e9"
},
"diffRemovedLineNumberBg": {
"dark": "#3c2b2b",
"light": "#ffebee"
},
"markdownText": {
"dark": "darkFg",
"light": "lightFg"
},
"markdownHeading": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownLink": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownLinkText": {
"dark": "darkPurple",
"light": "lightPurple"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "#37474f",
"light": "#e0e0e0"
},
"markdownListItem": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImageText": {
"dark": "darkPurple",
"light": "lightPurple"
},
"markdownCodeBlock": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxComment": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"syntaxKeyword": {
"dark": "darkPurple",
"light": "lightPurple"
},
"syntaxFunction": {
"dark": "darkBlue",
"light": "lightBlue"
},
"syntaxVariable": {
"dark": "darkFg",
"light": "lightFg"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkFg",
"light": "lightFg"
}
}
}
@@ -0,0 +1,221 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background": "#272822",
"backgroundAlt": "#1e1f1c",
"backgroundPanel": "#3e3d32",
"foreground": "#f8f8f2",
"comment": "#75715e",
"red": "#f92672",
"orange": "#fd971f",
"lightOrange": "#e69f66",
"yellow": "#e6db74",
"green": "#a6e22e",
"cyan": "#66d9ef",
"blue": "#66d9ef",
"purple": "#ae81ff",
"pink": "#f92672"
},
"theme": {
"primary": {
"dark": "cyan",
"light": "blue"
},
"secondary": {
"dark": "purple",
"light": "purple"
},
"accent": {
"dark": "green",
"light": "green"
},
"error": {
"dark": "red",
"light": "red"
},
"warning": {
"dark": "yellow",
"light": "orange"
},
"success": {
"dark": "green",
"light": "green"
},
"info": {
"dark": "orange",
"light": "orange"
},
"text": {
"dark": "foreground",
"light": "#272822"
},
"textMuted": {
"dark": "comment",
"light": "#75715e"
},
"background": {
"dark": "#272822",
"light": "#fafafa"
},
"backgroundPanel": {
"dark": "#1e1f1c",
"light": "#f0f0f0"
},
"backgroundElement": {
"dark": "#3e3d32",
"light": "#e0e0e0"
},
"border": {
"dark": "#3e3d32",
"light": "#d0d0d0"
},
"borderActive": {
"dark": "cyan",
"light": "blue"
},
"borderSubtle": {
"dark": "#1e1f1c",
"light": "#e8e8e8"
},
"diffAdded": {
"dark": "green",
"light": "green"
},
"diffRemoved": {
"dark": "red",
"light": "red"
},
"diffContext": {
"dark": "comment",
"light": "#75715e"
},
"diffHunkHeader": {
"dark": "comment",
"light": "#75715e"
},
"diffHighlightAdded": {
"dark": "green",
"light": "green"
},
"diffHighlightRemoved": {
"dark": "red",
"light": "red"
},
"diffAddedBg": {
"dark": "#1a3a1a",
"light": "#e0ffe0"
},
"diffRemovedBg": {
"dark": "#3a1a1a",
"light": "#ffe0e0"
},
"diffContextBg": {
"dark": "#1e1f1c",
"light": "#f0f0f0"
},
"diffLineNumber": {
"dark": "#3e3d32",
"light": "#d0d0d0"
},
"diffAddedLineNumberBg": {
"dark": "#1a3a1a",
"light": "#e0ffe0"
},
"diffRemovedLineNumberBg": {
"dark": "#3a1a1a",
"light": "#ffe0e0"
},
"markdownText": {
"dark": "foreground",
"light": "#272822"
},
"markdownHeading": {
"dark": "pink",
"light": "pink"
},
"markdownLink": {
"dark": "cyan",
"light": "blue"
},
"markdownLinkText": {
"dark": "purple",
"light": "purple"
},
"markdownCode": {
"dark": "green",
"light": "green"
},
"markdownBlockQuote": {
"dark": "comment",
"light": "#75715e"
},
"markdownEmph": {
"dark": "yellow",
"light": "orange"
},
"markdownStrong": {
"dark": "orange",
"light": "orange"
},
"markdownHorizontalRule": {
"dark": "comment",
"light": "#75715e"
},
"markdownListItem": {
"dark": "cyan",
"light": "blue"
},
"markdownListEnumeration": {
"dark": "purple",
"light": "purple"
},
"markdownImage": {
"dark": "cyan",
"light": "blue"
},
"markdownImageText": {
"dark": "purple",
"light": "purple"
},
"markdownCodeBlock": {
"dark": "foreground",
"light": "#272822"
},
"syntaxComment": {
"dark": "comment",
"light": "#75715e"
},
"syntaxKeyword": {
"dark": "pink",
"light": "pink"
},
"syntaxFunction": {
"dark": "green",
"light": "green"
},
"syntaxVariable": {
"dark": "foreground",
"light": "#272822"
},
"syntaxString": {
"dark": "yellow",
"light": "orange"
},
"syntaxNumber": {
"dark": "purple",
"light": "purple"
},
"syntaxType": {
"dark": "cyan",
"light": "blue"
},
"syntaxOperator": {
"dark": "pink",
"light": "pink"
},
"syntaxPunctuation": {
"dark": "foreground",
"light": "#272822"
}
}
}
@@ -243,4 +243,3 @@
}
}
}
@@ -0,0 +1,222 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background": "#292d3e",
"backgroundAlt": "#1e2132",
"backgroundPanel": "#32364a",
"foreground": "#a6accd",
"foregroundBright": "#bfc7d5",
"comment": "#676e95",
"red": "#f07178",
"orange": "#f78c6c",
"yellow": "#ffcb6b",
"green": "#c3e88d",
"cyan": "#89ddff",
"blue": "#82aaff",
"purple": "#c792ea",
"magenta": "#ff5370",
"pink": "#f07178"
},
"theme": {
"primary": {
"dark": "blue",
"light": "#4976eb"
},
"secondary": {
"dark": "purple",
"light": "#a854f2"
},
"accent": {
"dark": "cyan",
"light": "#00acc1"
},
"error": {
"dark": "red",
"light": "#e53935"
},
"warning": {
"dark": "yellow",
"light": "#ffb300"
},
"success": {
"dark": "green",
"light": "#91b859"
},
"info": {
"dark": "orange",
"light": "#f4511e"
},
"text": {
"dark": "foreground",
"light": "#292d3e"
},
"textMuted": {
"dark": "comment",
"light": "#8796b0"
},
"background": {
"dark": "#292d3e",
"light": "#fafafa"
},
"backgroundPanel": {
"dark": "#1e2132",
"light": "#f5f5f5"
},
"backgroundElement": {
"dark": "#32364a",
"light": "#e7e7e8"
},
"border": {
"dark": "#32364a",
"light": "#e0e0e0"
},
"borderActive": {
"dark": "blue",
"light": "#4976eb"
},
"borderSubtle": {
"dark": "#1e2132",
"light": "#eeeeee"
},
"diffAdded": {
"dark": "green",
"light": "#91b859"
},
"diffRemoved": {
"dark": "red",
"light": "#e53935"
},
"diffContext": {
"dark": "comment",
"light": "#8796b0"
},
"diffHunkHeader": {
"dark": "cyan",
"light": "#00acc1"
},
"diffHighlightAdded": {
"dark": "green",
"light": "#91b859"
},
"diffHighlightRemoved": {
"dark": "red",
"light": "#e53935"
},
"diffAddedBg": {
"dark": "#2e3c2b",
"light": "#e8f5e9"
},
"diffRemovedBg": {
"dark": "#3c2b2b",
"light": "#ffebee"
},
"diffContextBg": {
"dark": "#1e2132",
"light": "#f5f5f5"
},
"diffLineNumber": {
"dark": "#444760",
"light": "#cfd8dc"
},
"diffAddedLineNumberBg": {
"dark": "#2e3c2b",
"light": "#e8f5e9"
},
"diffRemovedLineNumberBg": {
"dark": "#3c2b2b",
"light": "#ffebee"
},
"markdownText": {
"dark": "foreground",
"light": "#292d3e"
},
"markdownHeading": {
"dark": "purple",
"light": "#a854f2"
},
"markdownLink": {
"dark": "blue",
"light": "#4976eb"
},
"markdownLinkText": {
"dark": "cyan",
"light": "#00acc1"
},
"markdownCode": {
"dark": "green",
"light": "#91b859"
},
"markdownBlockQuote": {
"dark": "comment",
"light": "#8796b0"
},
"markdownEmph": {
"dark": "yellow",
"light": "#ffb300"
},
"markdownStrong": {
"dark": "orange",
"light": "#f4511e"
},
"markdownHorizontalRule": {
"dark": "comment",
"light": "#8796b0"
},
"markdownListItem": {
"dark": "blue",
"light": "#4976eb"
},
"markdownListEnumeration": {
"dark": "cyan",
"light": "#00acc1"
},
"markdownImage": {
"dark": "blue",
"light": "#4976eb"
},
"markdownImageText": {
"dark": "cyan",
"light": "#00acc1"
},
"markdownCodeBlock": {
"dark": "foreground",
"light": "#292d3e"
},
"syntaxComment": {
"dark": "comment",
"light": "#8796b0"
},
"syntaxKeyword": {
"dark": "purple",
"light": "#a854f2"
},
"syntaxFunction": {
"dark": "blue",
"light": "#4976eb"
},
"syntaxVariable": {
"dark": "foreground",
"light": "#292d3e"
},
"syntaxString": {
"dark": "green",
"light": "#91b859"
},
"syntaxNumber": {
"dark": "orange",
"light": "#f4511e"
},
"syntaxType": {
"dark": "yellow",
"light": "#ffb300"
},
"syntaxOperator": {
"dark": "cyan",
"light": "#00acc1"
},
"syntaxPunctuation": {
"dark": "foreground",
"light": "#292d3e"
}
}
}
@@ -0,0 +1,234 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"base": "#191724",
"surface": "#1f1d2e",
"overlay": "#26233a",
"muted": "#6e6a86",
"subtle": "#908caa",
"text": "#e0def4",
"love": "#eb6f92",
"gold": "#f6c177",
"rose": "#ebbcba",
"pine": "#31748f",
"foam": "#9ccfd8",
"iris": "#c4a7e7",
"highlightLow": "#21202e",
"highlightMed": "#403d52",
"highlightHigh": "#524f67",
"moonBase": "#232136",
"moonSurface": "#2a273f",
"moonOverlay": "#393552",
"moonMuted": "#6e6a86",
"moonSubtle": "#908caa",
"moonText": "#e0def4",
"dawnBase": "#faf4ed",
"dawnSurface": "#fffaf3",
"dawnOverlay": "#f2e9e1",
"dawnMuted": "#9893a5",
"dawnSubtle": "#797593",
"dawnText": "#575279"
},
"theme": {
"primary": {
"dark": "foam",
"light": "pine"
},
"secondary": {
"dark": "iris",
"light": "#907aa9"
},
"accent": {
"dark": "rose",
"light": "#d7827e"
},
"error": {
"dark": "love",
"light": "#b4637a"
},
"warning": {
"dark": "gold",
"light": "#ea9d34"
},
"success": {
"dark": "pine",
"light": "#286983"
},
"info": {
"dark": "foam",
"light": "#56949f"
},
"text": {
"dark": "#e0def4",
"light": "#575279"
},
"textMuted": {
"dark": "muted",
"light": "dawnMuted"
},
"background": {
"dark": "base",
"light": "dawnBase"
},
"backgroundPanel": {
"dark": "surface",
"light": "dawnSurface"
},
"backgroundElement": {
"dark": "overlay",
"light": "dawnOverlay"
},
"border": {
"dark": "highlightMed",
"light": "#dfdad9"
},
"borderActive": {
"dark": "foam",
"light": "pine"
},
"borderSubtle": {
"dark": "highlightLow",
"light": "#f4ede8"
},
"diffAdded": {
"dark": "pine",
"light": "#286983"
},
"diffRemoved": {
"dark": "love",
"light": "#b4637a"
},
"diffContext": {
"dark": "muted",
"light": "dawnMuted"
},
"diffHunkHeader": {
"dark": "iris",
"light": "#907aa9"
},
"diffHighlightAdded": {
"dark": "pine",
"light": "#286983"
},
"diffHighlightRemoved": {
"dark": "love",
"light": "#b4637a"
},
"diffAddedBg": {
"dark": "#1f2d3a",
"light": "#e5f2f3"
},
"diffRemovedBg": {
"dark": "#3a1f2d",
"light": "#fce5e8"
},
"diffContextBg": {
"dark": "surface",
"light": "dawnSurface"
},
"diffLineNumber": {
"dark": "muted",
"light": "dawnMuted"
},
"diffAddedLineNumberBg": {
"dark": "#1f2d3a",
"light": "#e5f2f3"
},
"diffRemovedLineNumberBg": {
"dark": "#3a1f2d",
"light": "#fce5e8"
},
"markdownText": {
"dark": "#e0def4",
"light": "#575279"
},
"markdownHeading": {
"dark": "iris",
"light": "#907aa9"
},
"markdownLink": {
"dark": "foam",
"light": "pine"
},
"markdownLinkText": {
"dark": "rose",
"light": "#d7827e"
},
"markdownCode": {
"dark": "pine",
"light": "#286983"
},
"markdownBlockQuote": {
"dark": "muted",
"light": "dawnMuted"
},
"markdownEmph": {
"dark": "gold",
"light": "#ea9d34"
},
"markdownStrong": {
"dark": "love",
"light": "#b4637a"
},
"markdownHorizontalRule": {
"dark": "highlightMed",
"light": "#dfdad9"
},
"markdownListItem": {
"dark": "foam",
"light": "pine"
},
"markdownListEnumeration": {
"dark": "rose",
"light": "#d7827e"
},
"markdownImage": {
"dark": "foam",
"light": "pine"
},
"markdownImageText": {
"dark": "rose",
"light": "#d7827e"
},
"markdownCodeBlock": {
"dark": "#e0def4",
"light": "#575279"
},
"syntaxComment": {
"dark": "muted",
"light": "dawnMuted"
},
"syntaxKeyword": {
"dark": "pine",
"light": "#286983"
},
"syntaxFunction": {
"dark": "rose",
"light": "#d7827e"
},
"syntaxVariable": {
"dark": "#e0def4",
"light": "#575279"
},
"syntaxString": {
"dark": "gold",
"light": "#ea9d34"
},
"syntaxNumber": {
"dark": "iris",
"light": "#907aa9"
},
"syntaxType": {
"dark": "foam",
"light": "#56949f"
},
"syntaxOperator": {
"dark": "subtle",
"light": "dawnSubtle"
},
"syntaxPunctuation": {
"dark": "subtle",
"light": "dawnSubtle"
}
}
}
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"base03": "#002b36",
"base02": "#073642",
"base01": "#586e75",
"base00": "#657b83",
"base0": "#839496",
"base1": "#93a1a1",
"base2": "#eee8d5",
"base3": "#fdf6e3",
"yellow": "#b58900",
"orange": "#cb4b16",
"red": "#dc322f",
"magenta": "#d33682",
"violet": "#6c71c4",
"blue": "#268bd2",
"cyan": "#2aa198",
"green": "#859900"
},
"theme": {
"primary": {
"dark": "blue",
"light": "blue"
},
"secondary": {
"dark": "violet",
"light": "violet"
},
"accent": {
"dark": "cyan",
"light": "cyan"
},
"error": {
"dark": "red",
"light": "red"
},
"warning": {
"dark": "yellow",
"light": "yellow"
},
"success": {
"dark": "green",
"light": "green"
},
"info": {
"dark": "orange",
"light": "orange"
},
"text": {
"dark": "base0",
"light": "base00"
},
"textMuted": {
"dark": "base01",
"light": "base1"
},
"background": {
"dark": "base03",
"light": "base3"
},
"backgroundPanel": {
"dark": "base02",
"light": "base2"
},
"backgroundElement": {
"dark": "#073642",
"light": "#eee8d5"
},
"border": {
"dark": "base02",
"light": "base2"
},
"borderActive": {
"dark": "base01",
"light": "base1"
},
"borderSubtle": {
"dark": "#073642",
"light": "#eee8d5"
},
"diffAdded": {
"dark": "green",
"light": "green"
},
"diffRemoved": {
"dark": "red",
"light": "red"
},
"diffContext": {
"dark": "base01",
"light": "base1"
},
"diffHunkHeader": {
"dark": "base01",
"light": "base1"
},
"diffHighlightAdded": {
"dark": "green",
"light": "green"
},
"diffHighlightRemoved": {
"dark": "red",
"light": "red"
},
"diffAddedBg": {
"dark": "#073642",
"light": "#eee8d5"
},
"diffRemovedBg": {
"dark": "#073642",
"light": "#eee8d5"
},
"diffContextBg": {
"dark": "base02",
"light": "base2"
},
"diffLineNumber": {
"dark": "base01",
"light": "base1"
},
"diffAddedLineNumberBg": {
"dark": "#073642",
"light": "#eee8d5"
},
"diffRemovedLineNumberBg": {
"dark": "#073642",
"light": "#eee8d5"
},
"markdownText": {
"dark": "base0",
"light": "base00"
},
"markdownHeading": {
"dark": "blue",
"light": "blue"
},
"markdownLink": {
"dark": "cyan",
"light": "cyan"
},
"markdownLinkText": {
"dark": "violet",
"light": "violet"
},
"markdownCode": {
"dark": "green",
"light": "green"
},
"markdownBlockQuote": {
"dark": "base01",
"light": "base1"
},
"markdownEmph": {
"dark": "yellow",
"light": "yellow"
},
"markdownStrong": {
"dark": "orange",
"light": "orange"
},
"markdownHorizontalRule": {
"dark": "base01",
"light": "base1"
},
"markdownListItem": {
"dark": "blue",
"light": "blue"
},
"markdownListEnumeration": {
"dark": "cyan",
"light": "cyan"
},
"markdownImage": {
"dark": "cyan",
"light": "cyan"
},
"markdownImageText": {
"dark": "violet",
"light": "violet"
},
"markdownCodeBlock": {
"dark": "base0",
"light": "base00"
},
"syntaxComment": {
"dark": "base01",
"light": "base1"
},
"syntaxKeyword": {
"dark": "green",
"light": "green"
},
"syntaxFunction": {
"dark": "blue",
"light": "blue"
},
"syntaxVariable": {
"dark": "cyan",
"light": "cyan"
},
"syntaxString": {
"dark": "cyan",
"light": "cyan"
},
"syntaxNumber": {
"dark": "magenta",
"light": "magenta"
},
"syntaxType": {
"dark": "yellow",
"light": "yellow"
},
"syntaxOperator": {
"dark": "green",
"light": "green"
},
"syntaxPunctuation": {
"dark": "base0",
"light": "base00"
}
}
}
@@ -0,0 +1,226 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"background": "#262335",
"backgroundAlt": "#1e1a29",
"backgroundPanel": "#2a2139",
"foreground": "#ffffff",
"foregroundMuted": "#848bbd",
"pink": "#ff7edb",
"pinkBright": "#ff92df",
"cyan": "#36f9f6",
"cyanBright": "#72f1f8",
"yellow": "#fede5d",
"yellowBright": "#fff95d",
"orange": "#ff8b39",
"orangeBright": "#ff9f43",
"purple": "#b084eb",
"purpleBright": "#c792ea",
"red": "#fe4450",
"redBright": "#ff5e5b",
"green": "#72f1b8",
"greenBright": "#97f1d8"
},
"theme": {
"primary": {
"dark": "cyan",
"light": "#00bcd4"
},
"secondary": {
"dark": "pink",
"light": "#e91e63"
},
"accent": {
"dark": "purple",
"light": "#9c27b0"
},
"error": {
"dark": "red",
"light": "#f44336"
},
"warning": {
"dark": "yellow",
"light": "#ff9800"
},
"success": {
"dark": "green",
"light": "#4caf50"
},
"info": {
"dark": "orange",
"light": "#ff5722"
},
"text": {
"dark": "foreground",
"light": "#262335"
},
"textMuted": {
"dark": "foregroundMuted",
"light": "#5c5c8a"
},
"background": {
"dark": "#262335",
"light": "#fafafa"
},
"backgroundPanel": {
"dark": "#1e1a29",
"light": "#f5f5f5"
},
"backgroundElement": {
"dark": "#2a2139",
"light": "#eeeeee"
},
"border": {
"dark": "#495495",
"light": "#e0e0e0"
},
"borderActive": {
"dark": "cyan",
"light": "#00bcd4"
},
"borderSubtle": {
"dark": "#241b2f",
"light": "#f0f0f0"
},
"diffAdded": {
"dark": "green",
"light": "#4caf50"
},
"diffRemoved": {
"dark": "red",
"light": "#f44336"
},
"diffContext": {
"dark": "foregroundMuted",
"light": "#5c5c8a"
},
"diffHunkHeader": {
"dark": "purple",
"light": "#9c27b0"
},
"diffHighlightAdded": {
"dark": "greenBright",
"light": "#4caf50"
},
"diffHighlightRemoved": {
"dark": "redBright",
"light": "#f44336"
},
"diffAddedBg": {
"dark": "#1a3a2a",
"light": "#e8f5e9"
},
"diffRemovedBg": {
"dark": "#3a1a2a",
"light": "#ffebee"
},
"diffContextBg": {
"dark": "#1e1a29",
"light": "#f5f5f5"
},
"diffLineNumber": {
"dark": "#495495",
"light": "#b0b0b0"
},
"diffAddedLineNumberBg": {
"dark": "#1a3a2a",
"light": "#e8f5e9"
},
"diffRemovedLineNumberBg": {
"dark": "#3a1a2a",
"light": "#ffebee"
},
"markdownText": {
"dark": "foreground",
"light": "#262335"
},
"markdownHeading": {
"dark": "pink",
"light": "#e91e63"
},
"markdownLink": {
"dark": "cyan",
"light": "#00bcd4"
},
"markdownLinkText": {
"dark": "purple",
"light": "#9c27b0"
},
"markdownCode": {
"dark": "green",
"light": "#4caf50"
},
"markdownBlockQuote": {
"dark": "foregroundMuted",
"light": "#5c5c8a"
},
"markdownEmph": {
"dark": "yellow",
"light": "#ff9800"
},
"markdownStrong": {
"dark": "orange",
"light": "#ff5722"
},
"markdownHorizontalRule": {
"dark": "#495495",
"light": "#e0e0e0"
},
"markdownListItem": {
"dark": "cyan",
"light": "#00bcd4"
},
"markdownListEnumeration": {
"dark": "purple",
"light": "#9c27b0"
},
"markdownImage": {
"dark": "cyan",
"light": "#00bcd4"
},
"markdownImageText": {
"dark": "purple",
"light": "#9c27b0"
},
"markdownCodeBlock": {
"dark": "foreground",
"light": "#262335"
},
"syntaxComment": {
"dark": "foregroundMuted",
"light": "#5c5c8a"
},
"syntaxKeyword": {
"dark": "pink",
"light": "#e91e63"
},
"syntaxFunction": {
"dark": "orange",
"light": "#ff5722"
},
"syntaxVariable": {
"dark": "foreground",
"light": "#262335"
},
"syntaxString": {
"dark": "yellow",
"light": "#ff9800"
},
"syntaxNumber": {
"dark": "purple",
"light": "#9c27b0"
},
"syntaxType": {
"dark": "cyan",
"light": "#00bcd4"
},
"syntaxOperator": {
"dark": "pink",
"light": "#e91e63"
},
"syntaxPunctuation": {
"dark": "foreground",
"light": "#262335"
}
}
}
@@ -241,4 +241,3 @@
}
}
}
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"bg": "#3f3f3f",
"bgAlt": "#4f4f4f",
"bgPanel": "#5f5f5f",
"fg": "#dcdccc",
"fgMuted": "#9f9f9f",
"red": "#cc9393",
"redBright": "#dca3a3",
"green": "#7f9f7f",
"greenBright": "#8fb28f",
"yellow": "#f0dfaf",
"yellowDim": "#e0cf9f",
"blue": "#8cd0d3",
"blueDim": "#7cb8bb",
"magenta": "#dc8cc3",
"cyan": "#93e0e3",
"orange": "#dfaf8f"
},
"theme": {
"primary": {
"dark": "blue",
"light": "#5f7f8f"
},
"secondary": {
"dark": "magenta",
"light": "#8f5f8f"
},
"accent": {
"dark": "cyan",
"light": "#5f8f8f"
},
"error": {
"dark": "red",
"light": "#8f5f5f"
},
"warning": {
"dark": "yellow",
"light": "#8f8f5f"
},
"success": {
"dark": "green",
"light": "#5f8f5f"
},
"info": {
"dark": "orange",
"light": "#8f7f5f"
},
"text": {
"dark": "fg",
"light": "#3f3f3f"
},
"textMuted": {
"dark": "fgMuted",
"light": "#6f6f6f"
},
"background": {
"dark": "bg",
"light": "#ffffef"
},
"backgroundPanel": {
"dark": "bgAlt",
"light": "#f5f5e5"
},
"backgroundElement": {
"dark": "bgPanel",
"light": "#ebebdb"
},
"border": {
"dark": "#5f5f5f",
"light": "#d0d0c0"
},
"borderActive": {
"dark": "blue",
"light": "#5f7f8f"
},
"borderSubtle": {
"dark": "#4f4f4f",
"light": "#e0e0d0"
},
"diffAdded": {
"dark": "green",
"light": "#5f8f5f"
},
"diffRemoved": {
"dark": "red",
"light": "#8f5f5f"
},
"diffContext": {
"dark": "fgMuted",
"light": "#6f6f6f"
},
"diffHunkHeader": {
"dark": "cyan",
"light": "#5f8f8f"
},
"diffHighlightAdded": {
"dark": "greenBright",
"light": "#5f8f5f"
},
"diffHighlightRemoved": {
"dark": "redBright",
"light": "#8f5f5f"
},
"diffAddedBg": {
"dark": "#4f5f4f",
"light": "#efffef"
},
"diffRemovedBg": {
"dark": "#5f4f4f",
"light": "#ffefef"
},
"diffContextBg": {
"dark": "bgAlt",
"light": "#f5f5e5"
},
"diffLineNumber": {
"dark": "#6f6f6f",
"light": "#b0b0a0"
},
"diffAddedLineNumberBg": {
"dark": "#4f5f4f",
"light": "#efffef"
},
"diffRemovedLineNumberBg": {
"dark": "#5f4f4f",
"light": "#ffefef"
},
"markdownText": {
"dark": "fg",
"light": "#3f3f3f"
},
"markdownHeading": {
"dark": "yellow",
"light": "#8f8f5f"
},
"markdownLink": {
"dark": "blue",
"light": "#5f7f8f"
},
"markdownLinkText": {
"dark": "cyan",
"light": "#5f8f8f"
},
"markdownCode": {
"dark": "green",
"light": "#5f8f5f"
},
"markdownBlockQuote": {
"dark": "fgMuted",
"light": "#6f6f6f"
},
"markdownEmph": {
"dark": "yellowDim",
"light": "#8f8f5f"
},
"markdownStrong": {
"dark": "orange",
"light": "#8f7f5f"
},
"markdownHorizontalRule": {
"dark": "fgMuted",
"light": "#6f6f6f"
},
"markdownListItem": {
"dark": "blue",
"light": "#5f7f8f"
},
"markdownListEnumeration": {
"dark": "cyan",
"light": "#5f8f8f"
},
"markdownImage": {
"dark": "blue",
"light": "#5f7f8f"
},
"markdownImageText": {
"dark": "cyan",
"light": "#5f8f8f"
},
"markdownCodeBlock": {
"dark": "fg",
"light": "#3f3f3f"
},
"syntaxComment": {
"dark": "#7f9f7f",
"light": "#5f7f5f"
},
"syntaxKeyword": {
"dark": "yellow",
"light": "#8f8f5f"
},
"syntaxFunction": {
"dark": "blue",
"light": "#5f7f8f"
},
"syntaxVariable": {
"dark": "fg",
"light": "#3f3f3f"
},
"syntaxString": {
"dark": "red",
"light": "#8f5f5f"
},
"syntaxNumber": {
"dark": "greenBright",
"light": "#5f8f5f"
},
"syntaxType": {
"dark": "cyan",
"light": "#5f8f8f"
},
"syntaxOperator": {
"dark": "yellow",
"light": "#8f8f5f"
},
"syntaxPunctuation": {
"dark": "fg",
"light": "#3f3f3f"
}
}
}
+93 -2
View File
@@ -1088,13 +1088,17 @@
},
{
"$ref": "#/components/schemas/UnknownError"
},
{
"$ref": "#/components/schemas/MessageOutputLengthError"
}
],
"discriminator": {
"propertyName": "name",
"mapping": {
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
"UnknownError": "#/components/schemas/UnknownError"
"UnknownError": "#/components/schemas/UnknownError",
"MessageOutputLengthError": "#/components/schemas/MessageOutputLengthError"
}
}
},
@@ -1272,6 +1276,22 @@
"data"
]
},
"MessageOutputLengthError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "MessageOutputLengthError"
},
"data": {
"type": "object"
}
},
"required": [
"name",
"data"
]
},
"Event.message.part.updated": {
"type": "object",
"properties": {
@@ -1420,13 +1440,17 @@
},
{
"$ref": "#/components/schemas/UnknownError"
},
{
"$ref": "#/components/schemas/MessageOutputLengthError"
}
],
"discriminator": {
"propertyName": "name",
"mapping": {
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
"UnknownError": "#/components/schemas/UnknownError"
"UnknownError": "#/components/schemas/UnknownError",
"MessageOutputLengthError": "#/components/schemas/MessageOutputLengthError"
}
}
}
@@ -1441,9 +1465,15 @@
"App.Info": {
"type": "object",
"properties": {
"project": {
"type": "string"
},
"user": {
"type": "string"
},
"hostname": {
"type": "string"
},
"git": {
"type": "boolean"
},
@@ -1484,7 +1514,9 @@
}
},
"required": [
"project",
"user",
"hostname",
"git",
"path",
"time"
@@ -1644,6 +1676,65 @@
}
},
"description": "MCP (Model Context Protocol) server configurations"
},
"experimental": {
"type": "object",
"properties": {
"hook": {
"type": "object",
"properties": {
"file_edited": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"command"
]
}
}
},
"session_completed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"command"
]
}
}
}
}
}
}
},
"additionalProperties": false
+85 -5
View File
@@ -25,15 +25,17 @@ const (
// AppInfo defines model for App.Info.
type AppInfo struct {
Git bool `json:"git"`
Path struct {
Git bool `json:"git"`
Hostname string `json:"hostname"`
Path struct {
Config string `json:"config"`
Cwd string `json:"cwd"`
Data string `json:"data"`
Root string `json:"root"`
State string `json:"state"`
} `json:"path"`
Time struct {
Project string `json:"project"`
Time struct {
Initialized *float32 `json:"initialized,omitempty"`
} `json:"time"`
User string `json:"user"`
@@ -51,8 +53,20 @@ 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"`
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
Experimental *struct {
Hook *struct {
FileEdited *map[string][]struct {
Command []string `json:"command"`
Environment *map[string]string `json:"environment,omitempty"`
} `json:"file_edited,omitempty"`
SessionCompleted *[]struct {
Command []string `json:"command"`
Environment *map[string]string `json:"environment,omitempty"`
} `json:"session_completed,omitempty"`
} `json:"hook,omitempty"`
} `json:"experimental,omitempty"`
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
// Mcp MCP (Model Context Protocol) server configurations
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
@@ -434,6 +448,12 @@ type MessageToolInvocationToolResult struct {
ToolName string `json:"toolName"`
}
// MessageOutputLengthError defines model for MessageOutputLengthError.
type MessageOutputLengthError struct {
Data map[string]interface{} `json:"data"`
Name string `json:"name"`
}
// ModelInfo defines model for Model.Info.
type ModelInfo struct {
Attachment bool `json:"attachment"`
@@ -1110,6 +1130,34 @@ func (t *EventSessionError_Properties_Error) MergeUnknownError(v UnknownError) e
return err
}
// AsMessageOutputLengthError returns the union data inside the EventSessionError_Properties_Error as a MessageOutputLengthError
func (t EventSessionError_Properties_Error) AsMessageOutputLengthError() (MessageOutputLengthError, error) {
var body MessageOutputLengthError
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromMessageOutputLengthError overwrites any union data inside the EventSessionError_Properties_Error as the provided MessageOutputLengthError
func (t *EventSessionError_Properties_Error) FromMessageOutputLengthError(v MessageOutputLengthError) error {
v.Name = "MessageOutputLengthError"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeMessageOutputLengthError performs a merge with any union data inside the EventSessionError_Properties_Error, using the provided MessageOutputLengthError
func (t *EventSessionError_Properties_Error) MergeMessageOutputLengthError(v MessageOutputLengthError) error {
v.Name = "MessageOutputLengthError"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
func (t EventSessionError_Properties_Error) Discriminator() (string, error) {
var discriminator struct {
Discriminator string `json:"name"`
@@ -1124,6 +1172,8 @@ func (t EventSessionError_Properties_Error) ValueByDiscriminator() (interface{},
return nil, err
}
switch discriminator {
case "MessageOutputLengthError":
return t.AsMessageOutputLengthError()
case "ProviderAuthError":
return t.AsProviderAuthError()
case "UnknownError":
@@ -1199,6 +1249,34 @@ func (t *MessageMetadata_Error) MergeUnknownError(v UnknownError) error {
return err
}
// AsMessageOutputLengthError returns the union data inside the MessageMetadata_Error as a MessageOutputLengthError
func (t MessageMetadata_Error) AsMessageOutputLengthError() (MessageOutputLengthError, error) {
var body MessageOutputLengthError
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromMessageOutputLengthError overwrites any union data inside the MessageMetadata_Error as the provided MessageOutputLengthError
func (t *MessageMetadata_Error) FromMessageOutputLengthError(v MessageOutputLengthError) error {
v.Name = "MessageOutputLengthError"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeMessageOutputLengthError performs a merge with any union data inside the MessageMetadata_Error, using the provided MessageOutputLengthError
func (t *MessageMetadata_Error) MergeMessageOutputLengthError(v MessageOutputLengthError) error {
v.Name = "MessageOutputLengthError"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
func (t MessageMetadata_Error) Discriminator() (string, error) {
var discriminator struct {
Discriminator string `json:"name"`
@@ -1213,6 +1291,8 @@ func (t MessageMetadata_Error) ValueByDiscriminator() (interface{}, error) {
return nil, err
}
switch discriminator {
case "MessageOutputLengthError":
return t.AsMessageOutputLengthError()
case "ProviderAuthError":
return t.AsProviderAuthError()
case "UnknownError":
+26 -6
View File
@@ -8,14 +8,12 @@ import config from "./config.mjs"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
const url = "https://opencode.ai"
const github = "https://github.com/sst/opencode"
const headerLinks = [
{ name: "Docs", url: "/docs/" },
{ name: "GitHub", url: github },
]
// https://astro.build/config
export default defineConfig({
site: url,
output: "server",
adapter: cloudflare({
imageService: "passthrough",
@@ -34,8 +32,29 @@ export default defineConfig({
starlight({
title: "opencode",
expressiveCode: { themes: ["github-light", "github-dark"] },
social: [
{ icon: "github", label: "GitHub", href: config.github },
social: [{ icon: "github", label: "GitHub", href: config.github }],
head: [
{
tag: "link",
attrs: {
rel: "icon",
href: "/favicon.svg",
},
},
{
tag: "meta",
attrs: {
property: "og:image",
content: `${url}/social-share.png`,
},
},
{
tag: "meta",
attrs: {
property: "twitter:image",
content: `${url}/social-share.png`,
},
},
],
editLink: {
baseUrl: `${github}/edit/master/www/`,
@@ -52,6 +71,7 @@ export default defineConfig({
sidebar: [
"docs",
"docs/cli",
"docs/rules",
"docs/config",
"docs/models",
"docs/themes",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

@@ -0,0 +1,18 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,18 @@
<svg width="288" height="50" viewBox="0 0 288 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16.5H24V33H8V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M48 16.5H64V33H48V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M120 16.5H136V33H120V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M160 16.5H176V33H160V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M192 16.5H208V33H192V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M232 16.5H248V33H232V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M264 0H288V8.5H272V16.5H288V25H272V33H288V41.5H264V0Z" fill="black" fill-opacity="0.95"/>
<path d="M248 0H224V41.5H248V33H232V8.5H248V0Z" fill="black" fill-opacity="0.95"/>
<path d="M256 8.5H248V33H256V8.5Z" fill="black" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184 0H216V41.5H184V0ZM208 8.5H192V33H208V8.5Z" fill="black" fill-opacity="0.95"/>
<path d="M144 8.5H136V41.5H144V8.5Z" fill="black" fill-opacity="0.4"/>
<path d="M136 0H112V41.5H120V8.5H136V0Z" fill="black" fill-opacity="0.4"/>
<path d="M80 0H104V8.5H88V16.5H104V25H88V33H104V41.5H80V0Z" fill="black" fill-opacity="0.4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H72V41.5H48V49.5H40V0ZM64 8.5H48V33H64V8.5Z" fill="black" fill-opacity="0.4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="black" fill-opacity="0.4"/>
<path d="M152 0H176V8.5H160V33H176V41.5H152V0Z" fill="black" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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

+121 -70
View File
@@ -54,8 +54,8 @@ const DiffView: Component<DiffViewProps> = (props) => {
// Pair removals with additions
const maxLength = Math.max(removals.length, additions.length)
for (let k = 0; k < maxLength; k++) {
const hasLeft = !!removals[k]
const hasRight = !!additions[k]
const hasLeft = k < removals.length
const hasRight = k < additions.length
if (hasLeft && hasRight) {
// Replacement - left is removed, right is added
@@ -71,8 +71,8 @@ const DiffView: Component<DiffViewProps> = (props) => {
right: "",
type: "removed"
})
} else {
// Pure addition
} else if (hasRight) {
// Pure addition - only create if we actually have content
diffRows.push({
left: "",
right: additions[k],
@@ -111,85 +111,136 @@ const DiffView: Component<DiffViewProps> = (props) => {
return diffRows
})
const mobileRows = createMemo(() => {
const mobileBlocks: { type: 'removed' | 'added' | 'unchanged', lines: string[] }[] = []
const currentRows = rows()
let i = 0
while (i < currentRows.length) {
const removedLines: string[] = []
const addedLines: string[] = []
// Collect consecutive modified/removed/added rows
while (i < currentRows.length &&
(currentRows[i].type === 'modified' ||
currentRows[i].type === 'removed' ||
currentRows[i].type === 'added')) {
const row = currentRows[i]
if (row.left && (row.type === 'removed' || row.type === 'modified')) {
removedLines.push(row.left)
}
if (row.right && (row.type === 'added' || row.type === 'modified')) {
addedLines.push(row.right)
}
i++
}
// Add grouped blocks
if (removedLines.length > 0) {
mobileBlocks.push({ type: 'removed', lines: removedLines })
}
if (addedLines.length > 0) {
mobileBlocks.push({ type: 'added', lines: addedLines })
}
// Add unchanged rows as-is
if (i < currentRows.length && currentRows[i].type === 'unchanged') {
mobileBlocks.push({
type: 'unchanged',
lines: [currentRows[i].left]
})
i++
}
}
return mobileBlocks
})
return (
<div class={`${styles.diff} ${props.class ?? ""}`}>
{rows().map((r) => (
<div class={styles.row}>
<div class={styles.beforeColumn}>
<CodeBlock
code={r.left}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
data-display-mobile={r.type === "added" && !r.left ? "false" : undefined}
/>
{(r.type === "added" || r.type === "modified") && r.right !== undefined && (
<div class={styles.desktopView}>
{rows().map((r) => (
<div class={styles.row}>
<div class={styles.beforeColumn}>
<CodeBlock
code={r.left}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
/>
</div>
<div class={styles.afterColumn}>
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type="added"
data-display-mobile="true"
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
/>
)}
</div>
</div>
))}
</div>
<div class={styles.afterColumn}>
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
/>
<div class={styles.mobileView}>
{mobileRows().map((block) => (
<div class={styles.mobileBlock}>
{block.lines.map((line) => (
<CodeBlock
code={line}
lang={props.lang}
data-section="cell"
data-diff-type={block.type === 'removed' ? 'removed' :
block.type === 'added' ? 'added' : ''}
/>
))}
</div>
</div>
))}
))}
</div>
</div>
)
}
export default DiffView
// String to test diff viewer with
const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
+++ combined_after.txt 2025-06-24 16:38:12
@@ -1,21 +1,25 @@
unchanged line
-deleted line
-old content
+added line
+new content
-removed empty line below
+added empty line above
- tab indented
-trailing spaces
-very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
-unicode content: 🚀 ✨ 中文
-mixed content with tabs and spaces
+ space indented
+no trailing spaces
+short line
+very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
+different unicode: 🎉 💻 日本語
+normalized content with consistent spacing
+newline to content
-content to remove
-whitespace only:
-multiple
-consecutive
-deletions
-single deletion
+
+single addition
+first addition
+second addition
+third addition
line before addition
+first added line
+
+third added line
line after addition
final unchanged line`
// const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
// +++ combined_after.txt 2025-06-24 16:38:12
// @@ -1,21 +1,25 @@
// unchanged line
// -deleted line
// -old content
// +added line
// +new content
//
// -removed empty line below
// +added empty line above
//
// - tab indented
// -trailing spaces
// -very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
// -unicode content: 🚀 ✨ 中文
// -mixed content with tabs and spaces
// + space indented
// +no trailing spaces
// +short line
// +very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
// +different unicode: 🎉 💻 日本語
// +normalized content with consistent spacing
// +newline to content
//
// -content to remove
// -whitespace only:
// -multiple
// -consecutive
// -deletions
// -single deletion
// +
// +single addition
// +first addition
// +second addition
// +third addition
// line before addition
// +first added line
// +
// +third added line
// line after addition
// final unchanged line`
+48 -22
View File
@@ -5,7 +5,7 @@ import type { Props } from '@astrojs/starlight/props';
import CopyIcon from "../assets/lander/copy.svg";
import CheckIcon from "../assets/lander/check.svg";
import Screenshot from "../assets/themes/tokyonight.png";
import Screenshot from "../assets/lander/screenshot-splash.png";
const { data } = Astro.locals.starlightRoute.entry;
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
@@ -19,8 +19,10 @@ const imageAttrs = {
const github = config.social.filter(s => s.icon === 'github')[0];
const command = "npm i -g";
const pkg = "opencode-ai";
const command = "curl -fsSL"
const protocol = "https://"
const url = "opencode.ai/install"
const bash = "| bash"
let darkImage: ImageMetadata | undefined;
let lightImage: ImageMetadata | undefined;
@@ -51,11 +53,13 @@ if (image) {
<section class="cta">
<div class="col1">
<a href="/docs">View the docs</a>
<a href="/docs">Docs</a>
</div>
<div class="col2">
<button class="command" data-command={`${command} ${pkg}`}>
<code>{command}&nbsp;<span class="highlight">{pkg}</span></code>
<button class="command" data-command={`${command} ${protocol}${url} ${bash}`}>
<code>
<span>{command}&nbsp;</span><span class="protocol">{protocol}</span><span class="highlight">{url}</span>&nbsp;{bash}
</code>
<span class="copy">
<CopyIcon />
<CheckIcon />
@@ -63,7 +67,7 @@ if (image) {
</button>
</div>
<div class="col3">
<a href={github.href}>Star on GitHub</a>
<a href={github.href}>GitHub</a>
</div>
</section>
@@ -133,39 +137,43 @@ section.top {
section.cta {
display: flex;
flex-direction: row;
justify-content: space-between;
justify-content: flex-start;
align-items: stretch;
border-top: 2px solid var(--sl-color-border);
@media (max-width: 40rem) {
@media (max-width: 50rem) {
flex-direction: column;
& > div.col1 { order: 1; }
& > div.col3 { order: 2; }
& > div.col2 { order: 3; }
}
& > div {
flex: 1;
line-height: 1.4;
padding: calc(var(--padding) / 2) 0.5rem;
@media (max-width: 40rem) {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
padding: calc(var(--padding) / 2) 1rem;
a {
font-size: 1rem;
}
}
& > div.col2 {
@media (max-width: 50rem) {
flex: 0 0 auto;
}
}
& > div:not(.col2) {
& > div.col1, & > div.col3 {
flex: 1 1 auto;
text-align: center;
text-transform: uppercase;
@media (max-width: 50rem) {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
}
& > div.col2 {
flex: 0 0 auto;
}
& > div + div {
border-left: 2px solid var(--sl-color-border);
@media (max-width: 40rem) {
@media (max-width: 50rem) {
border-left: none;
border-top: 2px solid var(--sl-color-border);
}
@@ -183,6 +191,21 @@ section.cta {
code {
color: var(--sl-color-text-secondary);
font-size: 1.125rem;
@media (max-width: 24rem) {
font-size: 0.875rem;
}
@media (max-width: 30rem) {
span.protocol {
display: none;
}
}
@media (max-width: 43rem) {
text-align: center;
span:first-child {
display: block;
}
}
}
code .highlight {
color: var(--sl-color-text);
@@ -192,6 +215,9 @@ section.cta {
.copy {
line-height: 1;
padding: 0;
@media (max-width: 43rem) {
display: none;
}
}
.copy svg {
width: 1rem;
+11 -82
View File
@@ -23,7 +23,6 @@ import {
} from "./icons/custom"
import {
IconFolder,
IconCpuChip,
IconHashtag,
IconSparkles,
IconGlobeAlt,
@@ -85,7 +84,7 @@ function scrollToAnchor(id: string) {
el.scrollIntoView({ behavior: "smooth" })
}
function stripWorkingDirectory(filePath: string, workingDir?: string) {
function stripWorkingDirectory(filePath?: string, workingDir?: string) {
if (filePath === undefined || workingDir === undefined) return filePath
const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
@@ -102,10 +101,7 @@ function stripWorkingDirectory(filePath: string, workingDir?: string) {
}
function getShikiLang(filename: string) {
const ext = filename
.split('.')
.pop()
?.toLowerCase() ?? ''
const ext = filename.split(".").pop()?.toLowerCase() ?? ""
// map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript'])
const langs = map.languages(ext)
@@ -113,12 +109,10 @@ function getShikiLang(filename: string) {
// Overrride any specific language mappings
const overrides: Record<string, string> = {
"conf": "shellscript"
conf: "shellscript",
}
return type
? overrides[type] ?? type
: 'plaintext'
return type ? (overrides[type] ?? type) : "plaintext"
}
function formatDuration(ms: number): string {
@@ -1008,13 +1002,6 @@ export default function Share(props: {
}
>
{(assistant) => {
const system = createMemo(() => {
const prompts = assistant().system || []
return prompts.filter(
(p: string) =>
!p.startsWith("You are Claude"),
)
})
return (
<div
id={anchor()}
@@ -1040,67 +1027,13 @@ export default function Share(props: {
<span data-part-model>
{assistant().modelID}
</span>
<Show when={system().length > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show system prompt"
hideCopy="Hide system prompt"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={system().join("\n\n").trim()}
/>
</Show>
</div>
</Show>
</div>
</div>
</div>
)
}}
</Match>
{/* System text */}
<Match
when={
msg.role === "system" &&
part.type === "text" &&
part
}
>
{(part) => (
<div
id={anchor()}
data-section="part"
data-part-type="system-text"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconCpuChip width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>System</span>
</div>
<TextPart
data-size="sm"
text={part().text}
data-color="dimmed"
/>
</div>
</div>
</div>
)}
</Match>
{/* Grep tool */}
<Match
when={
@@ -1458,7 +1391,7 @@ export default function Share(props: {
{(_part) => {
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args.filePath,
toolData()?.args?.filePath,
data().rootDir,
),
)
@@ -1497,7 +1430,7 @@ export default function Share(props: {
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result,
toolData()?.result
)}
</ErrorPart>
</div>
@@ -1516,7 +1449,7 @@ export default function Share(props: {
<div data-part-tool-code>
<CodeBlock
lang={getShikiLang(filePath())}
code={args.content}
code={toolData()?.args?.content}
/>
</div>
</Show>
@@ -1658,8 +1591,7 @@ export default function Share(props: {
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName ===
"todowrite" &&
part.toolInvocation.toolName === "todowrite" &&
part
}
>
@@ -1724,8 +1656,7 @@ export default function Share(props: {
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName ===
"webfetch" &&
part.toolInvocation.toolName === "webfetch" &&
part
}
>
@@ -1899,9 +1830,7 @@ export default function Share(props: {
>
<IconSparkles width={18} height={18} />
</Match>
<Match when={msg.role === "system"}>
<IconCpuChip width={18} height={18} />
</Match>
<Match when={msg.role === "user"}>
<IconUserCircle width={18} height={18} />
</Match>
+16 -17
View File
@@ -6,6 +6,19 @@
border-radius: 0.25rem;
}
.desktopView {
display: block;
}
.mobileView {
display: none;
}
.mobileBlock {
display: flex;
flex-direction: column;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -98,25 +111,11 @@
}
@media (max-width: 40rem) {
.row {
grid-template-columns: 1fr;
}
.afterColumn {
.desktopView {
display: none;
}
.beforeColumn {
border-right: none;
}
[data-section="cell"] {
&[data-display-mobile="true"] {
display: flex;
}
&[data-display-mobile="false"] {
display: none;
}
.mobileView {
display: block;
}
}
@@ -30,6 +30,8 @@
h4,
h5,
h6 {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { Tabs, TabItem } from '@astrojs/starlight/components';
- 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.
![opencode TUI with the opencode theme](../../../assets/themes/opencode.png)
![opencode TUI with the opencode theme](../../../assets/lander/screenshot.png)
---
@@ -0,0 +1,74 @@
---
title: Rules
---
You can provide custom instructions to opencode by creating an `AGENTS.md` file. This is similar to `CLAUDE.md` or Cursor's rules. It contains instructions that will be included in the LLM's context to customize its behavior for your specific project.
---
## Initialize
To create a new `AGENTS.md` file, you can run the `/init` command in opencode.
:::tip
You should commit your project's `AGENTS.md` file to Git.
:::
This will scan your project and all its contents to understand what the project is about and generate an `AGENTS.md` file with it. This helps opencode to navigate the project better.
If you have an existing `AGENTS.md` file, this will try to add to it.
---
## Example
You can also just create this file manually. Here's an example of some things you can put into an `AGENTS.md` file.
```markdown title="AGENTS.md"
# SST v3 Monorepo Project
This is an SST v3 monorepo with TypeScript. The project uses bun workspaces for package management.
## Project Structure
- `packages/` - Contains all workspace packages (functions, core, web, etc.)
- `infra/` - Infrastructure definitions split by service (storage.ts, api.ts, web.ts)
- `sst.config.ts` - Main SST configuration with dynamic imports
## Code Standards
- Use TypeScript with strict mode enabled
- Shared code goes in `packages/core/` with proper exports configuration
- Functions go in `packages/functions/`
- Infrastructure should be split into logical files in `infra/`
## Monorepo Conventions
- Import shared modules using workspace names: `@my-app/core/example`
```
We are adding project-specific instructions here and this will be shared across your team.
---
## Types
opencode also supports reading the `AGENTS.md` file from multiple locations. And this serves different purposes.
### Project
The ones we have seen above, where the `AGENTS.md` is placed in the project root, are project-specific rules. These only apply when you are working in this directory or its sub-directories.
### Global
You can also have global rules in a `~/.config/opencode/AGENTS.md` file. This gets applied across all opencode sessions.
Since this isn't committed to Git or shared with your team, we recommend using this to specify any personal rules that the LLM should follow.
---
## Precedence
So when opencode starts, it looks for:
1. **Local files** by traversing up from the current directory
2. **Global file** by checking `~/.config/opencode/AGENTS.md`
If you have both global and project-specific rules, opencode will combine them together.
+83 -57
View File
@@ -2,34 +2,96 @@
title: Themes
---
With opencode you can select from one of several built-in themes, use a theme that adapts to your terminal theme, or define your own custom theme.
By default, opencode uses our own `opencode` theme.
---
## Built-in themes
opencode comes with several built-in themes.
| Name | Description |
| --- | --- |
| `system` | Adapts to your terminal's background color |
| `tokyonight` | Based on the Tokyonight theme |
| `everforest` | Based on the Everforest theme |
| `ayu` | Based on the Ayu dark theme |
| `catppuccin` | Based on the Catppuccin theme |
| `gruvbox` | Based on the Gruvbox theme |
| `kanagawa` | Based on the Kanagawa theme |
| `nord` | Based on the Nord theme |
| `matrix` | Hacker-style green on black theme |
| `one-dark` | Based on the Atom One Dark theme |
And more, we are constantly adding new themes.
---
## System theme
The `system` theme is designed to automatically adapt to your terminal's color scheme. Unlike traditional themes that use fixed colors, the _system_ theme:
- **Generates gray scale**: Creates a custom gray scale based on your terminal's background color, ensuring optimal contrast.
- **Uses ANSI colors**: Leverages standard ANSI colors (0-15) for syntax highlighting and UI elements, which respect your terminal's color palette.
- **Preserves terminal defaults**: Uses `none` for text and background colors to maintain your terminal's native appearance.
The system theme is for users who:
- Want opencode to match their terminal's appearance
- Use custom terminal color schemes
- Prefer a consistent look across all terminal applications
---
## Using a theme
You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in your [config](/docs/config).
```json title="opencode.json" {3}
{
"$schema": "https://opencode.ai/config.json",
"theme": "tokyonight"
}
```
---
## Custom themes
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
## Theme Loading Hierarchy
### Hierarchy
Themes are loaded from multiple directories in the following order (later directories override earlier ones):
Themes are loaded from multiple directories in the following order where later directories override earlier ones:
1. **Built-in themes** - Embedded in the binary
2. **User config directory** - `~/.config/opencode/themes/*.json` (or `$XDG_CONFIG_HOME/opencode/themes/*.json`)
3. **Project root directory** - `<project-root>/.opencode/themes/*.json`
4. **Current working directory** - `./.opencode/themes/*.json`
1. **Built-in themes** - These are embedded in the binary
2. **User config directory** - Defined in `~/.config/opencode/themes/*.json` or `$XDG_CONFIG_HOME/opencode/themes/*.json`
3. **Project root directory** - Defined in the `<project-root>/.opencode/themes/*.json`
4. **Current working directory** - Defined in `./.opencode/themes/*.json`
If multiple directories contain a theme with the same name, the theme from the directory with higher priority will be used.
## Creating a Custom Theme
### Creating a theme
To create a custom theme, create a JSON file in one of the theme directories:
To create a custom theme, create a JSON file in one of the theme directories.
For user-wide themes:
```bash no-frame
# For user-wide themes
mkdir -p ~/.config/opencode/themes
vim ~/.config/opencode/themes/my-theme.json
```
# For project-specific themes
And for project-specific themes.
```bash no-frame
mkdir -p .opencode/themes
vim .opencode/themes/my-theme.json
```
## Theme JSON Format
### JSON format
Themes use a flexible JSON format with support for:
@@ -37,11 +99,13 @@ Themes use a flexible JSON format with support for:
- **ANSI colors**: `3` (0-255)
- **Color references**: `"primary"` or custom definitions
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
- **No color**: `"none"` - Uses the terminal's default color (transparent)
- **No color**: `"none"` - Uses the terminal's default color or transparent
### Example Theme
### Example
```json no-frame
Here's an example of a custom theme:
```json title="my-theme.json"
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
@@ -267,51 +331,13 @@ Themes use a flexible JSON format with support for:
}
```
### Color Definitions
### Color definitions
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
The `defs` section is optional and it allows you to define reusable colors that can be referenced in the theme.
### Using "none" for Terminal Defaults
### Terminal defaults
The special value `\"none\"` can be used for any color to inherit the terminal's default color. This is particularly useful for creating themes that blend seamlessly with your terminal's color scheme:
- `\"text\": \"none\"` - Uses terminal's default foreground color
- `\"background\": \"none\"` - Uses terminal's default background color
## The System Theme
The `system` theme is opencode's default theme, designed to automatically adapt to your terminal's color scheme. Unlike traditional themes that use fixed colors, the system theme:
- **Generates gray scale**: Creates a custom gray scale based on your terminal's background color, ensuring optimal contrast
- **Uses ANSI colors**: Leverages standard ANSI colors (0-15) for syntax highlighting and UI elements, which respect your terminal's color palette
- **Preserves terminal defaults**: Uses `none` for text and background colors to maintain your terminal's native appearance
The system theme is ideal for users who:
- Want opencode to match their terminal's appearance
- Use custom terminal color schemes
- Prefer a consistent look across all terminal applications
## Built-in Themes
opencode comes with several built-in themes:
- `system` - Default theme that dynamically adapts to your terminal's background color
- `tokyonight` - Tokyonight theme
- `everforest` - Everforest theme
- `ayu` - Ayu dark theme
- `catppuccin` - Catppuccin theme
- `gruvbox` - Gruvbox theme
- `kanagawa` - Kanagawa theme
- `nord` - Nord theme
- `matrix` - Hacker-style green on black theme
- `one-dark` - Atom One Dark inspired theme
## Using a Theme
To use a theme, set it in your opencode configuration or select it from the theme dialog in the TUI.
```json title="opencode.json" {3}
{
"$schema": "https://opencode.ai/config.json",
"theme": "tokyonight"
}
```
- `"text": "none"` - Uses terminal's default foreground color
- `"background": "none"` - Uses terminal's default background color
+2 -2
View File
@@ -6,7 +6,7 @@ hero:
title: The AI coding agent built for the terminal.
tagline: The AI coding agent built for the terminal.
image:
dark: ../../assets/logo-dark.svg
light: ../../assets/logo-light.svg
dark: ../../assets/logo-ornate-dark.svg
light: ../../assets/logo-ornate-light.svg
alt: opencode logo
---
+27
View File
@@ -0,0 +1,27 @@
declare module "lang-map" {
/** Returned by calling `map()` */
export interface MapReturn {
/** All extensions keyed by language name */
extensions: Record<string, string[]>;
/** All languages keyed by file-extension */
languages: Record<string, string[]>;
}
/**
* Calling `map()` gives you the raw lookup tables:
*
* ```js
* const { extensions, languages } = map();
* ```
*/
function map(): MapReturn;
/** Static method: get extensions for a given language */
namespace map {
function extensions(language: string): string[];
/** Static method: get languages for a given extension */
function languages(extension: string): string[];
}
export = map;
}
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
if [ ! -d ".git" ]; then
exit 0