Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
f64ded7
chore: format code
actions-user Dec 1, 2025
ad111a7
wip: option 2 for JSONC user themes.
ariane-emory Dec 1, 2025
6dc434d
wip: option 2 for JSONC user themes.
ariane-emory Dec 1, 2025
f526449
fix: always use JSONC parser for user themes for consistency with how…
ariane-emory Dec 1, 2025
996a6d9
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 1, 2025
cddc656
refactor: more expressive parameter name (enableSpecialFeatures->enab…
ariane-emory Dec 1, 2025
2991548
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 1, 2025
a9d1f71
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 2, 2025
743ded2
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 2, 2025
ef32396
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 2, 2025
0a6e500
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 2, 2025
6540682
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 3, 2025
8e1174b
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 3, 2025
c66b0db
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 3, 2025
bac0201
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 3, 2025
d280c6c
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 4, 2025
f3e3d86
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 4, 2025
69fe71e
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 5, 2025
330f3d0
Merge branch 'dev' into feat/jsonc-user-themes-take-5
ariane-emory Dec 5, 2025
ffb583b
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-them…
ariane-emory Dec 5, 2025
6c53b94
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 6, 2025
f48871c
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 6, 2025
9abd457
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 7, 2025
ba8048b
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 7, 2025
ea11b85
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 7, 2025
ce40025
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 7, 2025
f08bbe9
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 8, 2025
8b88ddd
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 8, 2025
d924de4
Fix TypeScript error: remove cacheKey from FileContents interface usage
ariane-emory Dec 8, 2025
59cf724
fix: revert damaged file
ariane-emory Dec 8, 2025
49ee074
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 9, 2025
f4a4c3c
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 9, 2025
c172780
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 9, 2025
e12f71d
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
1a0298c
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
c274742
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
c4aa9ca
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
4074dbd
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
e8f70c9
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
151673a
Fix type error: useKittyKeyboard should be boolean
ariane-emory Dec 10, 2025
a403a95
Merge remote-tracking branch 'upstream/dev' into feat/jsonc-user-themes
ariane-emory Dec 10, 2025
d1772ff
fix: uncorrupt
ariane-emory Dec 11, 2025
711045a
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 11, 2025
e596b82
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 11, 2025
d3ed104
Merge branch 'dev' into repair/feat/jsonc-user-themes
ariane-emory Dec 13, 2025
46a36ab
style: remove some comments to match existing style
ariane-emory Dec 14, 2025
bc4b44d
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 17, 2025
575533e
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 17, 2025
8b93c36
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 18, 2025
23d9783
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 19, 2025
75d2886
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 19, 2025
b004ea7
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 20, 2025
376cd3a
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 20, 2025
6774cd2
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 21, 2025
a51e510
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 21, 2025
5414a90
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 22, 2025
eeeab12
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
b375885
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
f94bf9a
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
28b5639
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
5be3015
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
f948e65
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 23, 2025
b3ffcfb
Merge branch 'feat/jsonc-user-themes' of github.com:ariane-emory/open…
ariane-emory Dec 23, 2025
ac340ec
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 24, 2025
2d30035
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 24, 2025
95aee38
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 24, 2025
ef06f35
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 24, 2025
6603976
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 24, 2025
afc28ec
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 25, 2025
19d0547
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 25, 2025
1809e78
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 25, 2025
dd194c1
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 27, 2025
22dce0f
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 28, 2025
540563c
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 28, 2025
6f6089d
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 28, 2025
ea13f38
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 29, 2025
7555ad7
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 29, 2025
fe7317c
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 29, 2025
edb42ba
Merge branch 'feat/jsonc-user-themes' of github.com:ariane-emory/open…
ariane-emory Dec 29, 2025
3663f03
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 29, 2025
e54bc2f
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 29, 2025
9fa0823
Merge branch 'feat/jsonc-user-themes' of github.com:ariane-emory/open…
ariane-emory Dec 29, 2025
f3a88c9
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 30, 2025
5d6ec5d
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 30, 2025
5552f12
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 30, 2025
6b2a1c9
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 30, 2025
ca5da09
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 30, 2025
57dabfe
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Dec 31, 2025
43aad72
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 1, 2026
399c01c
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 1, 2026
fd05b39
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 1, 2026
43f7cd4
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 2, 2026
16632e6
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 3, 2026
d3dbe25
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 3, 2026
79adb59
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 4, 2026
e2d341e
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 4, 2026
c51c948
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 4, 2026
e1a27ec
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 5, 2026
f2445d8
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 5, 2026
7ef78ea
Merge branch 'feat/jsonc-user-themes' of github.com:ariane-emory/open…
ariane-emory Jan 5, 2026
5594b1d
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 5, 2026
1e60eb6
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 5, 2026
1a9e29a
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 5, 2026
b5433ec
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 6, 2026
2fc54e6
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 6, 2026
d0842ef
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 6, 2026
c20ea08
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 6, 2026
a48c9b4
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 6, 2026
8e02275
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 7, 2026
c87c889
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 7, 2026
0b19ea8
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 7, 2026
3a0d325
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 7, 2026
e587b08
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 8, 2026
6320565
Merge branch 'dev' into feat/jsonc-user-themes
ariane-emory Jan 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/opencode/src/cli/cmd/tui/context/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { Config } from "../../../../config/config"
import { useSDK } from "./sdk"

type ThemeColors = {
Expand Down Expand Up @@ -127,7 +128,7 @@ type Variant = {
light: HexColor | RefName
}
type ColorValue = HexColor | RefName | Variant | RGBA
type ThemeJson = {
export type ThemeJson = {
$schema?: string
defs?: Record<string, HexColor | RefName>
theme: Omit<Record<keyof ThemeColors, ColorValue>, "selectedListItemText" | "backgroundMenu"> & {
Expand Down Expand Up @@ -390,7 +391,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
},
})

const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.{json,jsonc}")
async function getCustomThemes() {
const directories = [
Global.Path.config,
Expand All @@ -410,8 +411,11 @@ async function getCustomThemes() {
dot: true,
cwd: dir,
})) {
const name = path.basename(item, ".json")
result[name] = await Bun.file(item).json()
const ext = path.extname(item)
const name = path.basename(item, ext)

// Use JSONC parser for all theme files regardless of extension
result[name] = await Config.loadThemeFile(item)
}
}
return result
Expand Down
120 changes: 83 additions & 37 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { NamedError } from "@opencode-ai/util/error"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
import type { ThemeJson } from "../cli/cmd/tui/context/theme"
import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun"
Expand Down Expand Up @@ -1094,45 +1095,48 @@ export namespace Config {
return load(text, filepath)
}

async function load(text: string, configFilepath: string) {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})

const fileMatches = text.match(/\{file:[^}]+\}/g)
if (fileMatches) {
const configDir = path.dirname(configFilepath)
const lines = text.split("\n")
async function load(text: string, configFilepath: string, enableConfigSubstitutions: boolean = true) {
if (enableConfigSubstitutions) {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
}

for (const match of fileMatches) {
const lineIndex = lines.findIndex((line) => line.includes(match))
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
continue // Skip if line is commented
}
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
if (enableConfigSubstitutions) {
const fileMatches = text.match(/\{file:[^}]+\}/g)
if (fileMatches) {
const configDir = path.dirname(configFilepath)
const lines = text.split("\n")

for (const match of fileMatches) {
const lineIndex = lines.findIndex((line) => line.includes(match))
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
continue
}
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (
await Bun.file(resolvedPath)
.text()
.catch((error) => {
const errMsg = `bad file reference: "${match}"`
if (error.code === "ENOENT") {
throw new InvalidError(
{
path: configFilepath,
message: errMsg + ` ${resolvedPath} does not exist`,
},
{ cause: error },
)
}
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
})
).trim()
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (
await Bun.file(resolvedPath)
.text()
.catch((error) => {
const errMsg = `bad file reference: "${match}"`
if (error.code === "ENOENT") {
throw new InvalidError(
{
path: configFilepath,
message: errMsg + ` ${resolvedPath} does not exist`,
},
{ cause: error },
)
}
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
})
).trim()
// escape newlines/quotes, strip outer quotes
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
}
}

Expand Down Expand Up @@ -1213,6 +1217,48 @@ export namespace Config {
return state().then((x) => x.config)
}

export async function loadThemeFile(filepath: string): Promise<ThemeJson> {
log.info("loading theme", { path: filepath })
let text = await Bun.file(filepath)
.text()
.catch((err) => {
if (err.code === "ENOENT") return
throw new JsonError({ path: filepath }, { cause: err })
})
if (!text) {
throw new Error("Empty theme file")
}

// Parse JSONC directly without special features for themes
const errors: JsoncParseError[] = []
const data = parseJsonc(text, errors, { allowTrailingComma: true })

if (errors.length) {
const lines = text.split("\n")
const errorDetails = errors
.map((e) => {
const beforeOffset = text.substring(0, e.offset).split("\n")
const line = beforeOffset.length
const column = beforeOffset[beforeOffset.length - 1].length + 1
const problemLine = lines[line - 1]

const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
if (!problemLine) return error

return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
.join("\n")

throw new JsonError({
path: filepath,
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
})
}

// Return data as ThemeJson (basic validation)
return data as ThemeJson
}

export async function update(config: Info) {
const filepath = path.join(Instance.directory, "config.json")
const existing = await loadFile(filepath)
Expand Down
115 changes: 115 additions & 0 deletions packages/opencode/test/config/theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test"
import { tmpdir } from "os"
import { join } from "path"
import { Config } from "../../src/config/config"
import { writeFileSync, mkdirSync, rmSync } from "fs"

describe("Theme Loading", () => {
let tempDir: string

beforeAll(() => {
tempDir = join(tmpdir(), "opencode-theme-test")
mkdirSync(tempDir, { recursive: true })
})

afterAll(() => {
rmSync(tempDir, { recursive: true, force: true })
})

test("should load JSONC theme file with comments", async () => {
const themeContent = `{
// This is a comment
"$schema": "https://opencode.ai/theme.json",
"theme": {
"primary": "#ff0000",
"secondary": "#00ff00"
}
}`

const themeFile = join(tempDir, "test-theme.jsonc")
writeFileSync(themeFile, themeContent)

const theme = await Config.loadThemeFile(themeFile)

expect(theme.theme.primary).toBe("#ff0000")
expect(theme.theme.secondary).toBe("#00ff00")
})

test("should NOT process environment variables in themes", async () => {
process.env.TEST_COLOR = "#00ff00"
const themeContent = `{
"theme": {
"primary": "{env:TEST_COLOR}",
"secondary": "#ff0000"
}
}`

const themeFile = join(tempDir, "test-theme.jsonc")
writeFileSync(themeFile, themeContent)

const theme = await Config.loadThemeFile(themeFile)

// Environment variable should NOT be processed in themes
expect(theme.theme.primary).toBe("{env:TEST_COLOR}")
expect(theme.theme.secondary).toBe("#ff0000")
})

test("should NOT process file inclusion in themes", async () => {
const colorFile = join(tempDir, "color.txt")
writeFileSync(colorFile, "#00ff00")

const themeContent = `{
"theme": {
"primary": "{file:color.txt}",
"secondary": "#ff0000"
}
}`

const themeFile = join(tempDir, "test-theme.jsonc")
writeFileSync(themeFile, themeContent)

const theme = await Config.loadThemeFile(themeFile)

// File inclusion should NOT be processed in themes
expect(theme.theme.primary).toBe("{file:color.txt}")
expect(theme.theme.secondary).toBe("#ff0000")
})

test("should handle trailing commas in JSONC themes", async () => {
const themeContent = `{
"theme": {
"primary": "#ff0000",
"secondary": "#00ff00", // Trailing comma
}
}`

const themeFile = join(tempDir, "test-theme.jsonc")
writeFileSync(themeFile, themeContent)

const theme = await Config.loadThemeFile(themeFile)

expect(theme.theme.primary).toBe("#ff0000")
expect(theme.theme.secondary).toBe("#00ff00")
})

test("should throw error for invalid JSONC theme", async () => {
const themeContent = `{
"theme": {
"primary": "#ff0000",
// Missing closing brace
"secondary": "#00ff00",
}`

const themeFile = join(tempDir, "test-theme.jsonc")
writeFileSync(themeFile, themeContent)

expect(Config.loadThemeFile(themeFile)).rejects.toThrow()
})

test("should throw error for empty theme file", async () => {
const themeFile = join(tempDir, "empty-theme.jsonc")
writeFileSync(themeFile, "")

expect(Config.loadThemeFile(themeFile)).rejects.toThrow("Empty theme file")
})
})