From 5397cabd457f002e1ef841e8547ef187bd371ee1 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 02:35:13 -0500 Subject: [PATCH 01/20] feat: add experimental.skip_models_fetch setting to allowdisabling automatic refreshes from models.dev in environments it is inaccessible for whatever reason' --- packages/opencode/src/config/config.ts | 4 ++ packages/opencode/src/provider/models.ts | 19 ++++++-- packages/opencode/test/config/config.test.ts | 46 ++++++++++++++++++++ packages/sdk/go/config.go | 2 + packages/sdk/js/src/gen/types.gen.ts | 4 ++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2c691cedb5f..48cba0a5427 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -674,6 +674,10 @@ export namespace Config { .array(z.string()) .optional() .describe("Tools that should only be available to primary agents."), + skip_models_fetch: z + .boolean() + .optional() + .describe("Skip automatic fetching of model information from models.dev on startup and timer"), }) .optional(), }) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 3d28787c88f..c8bc72f4096 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -4,10 +4,12 @@ import path from "path" import z from "zod" import { data } from "./models-macro" with { type: "macro" } import { Installation } from "../installation" +import { Config } from "../config/config" export namespace ModelsDev { const log = Log.create({ service: "models.dev" }) const filepath = path.join(Global.Path.cache, "models.json") + let timerSetup = false export const Model = z.object({ id: z.string(), @@ -62,8 +64,21 @@ export namespace ModelsDev { export type Provider = z.infer + async function setupTimer() { + if (timerSetup) return + timerSetup = true + const globalConfig = await Config.global() + if (!globalConfig.experimental?.skip_models_fetch) { + setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() + } + } + export async function get() { - refresh() + setupTimer() + const globalConfig = await Config.global() + if (!globalConfig.experimental?.skip_models_fetch) { + refresh() + } const file = Bun.file(filepath) const result = await file.json().catch(() => {}) if (result) return result as Record @@ -89,5 +104,3 @@ export namespace ModelsDev { if (result && result.ok) await Bun.write(file, await result.text()) } } - -setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 2ff8c01cdb0..a7dbde887a9 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -501,3 +501,49 @@ test("deduplicates duplicate plugins from global and local configs", async () => }, }) }) + +test("handles experimental.skip_models_fetch configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + experimental: { + skip_models_fetch: true, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.experimental?.skip_models_fetch).toBe(true) + }, + }) +}) + +test("handles experimental.skip_models_fetch set to false", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + experimental: { + skip_models_fetch: false, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.experimental?.skip_models_fetch).toBe(false) + }, + }) +}) diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 02460fb5df9..268dbcd68b4 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -705,6 +705,7 @@ func (r configCommandJSON) RawJSON() string { type ConfigExperimental struct { DisablePasteSummary bool `json:"disable_paste_summary"` Hook ConfigExperimentalHook `json:"hook"` + SkipModelsFetch bool `json:"skip_models_fetch"` JSON configExperimentalJSON `json:"-"` } @@ -713,6 +714,7 @@ type ConfigExperimental struct { type configExperimentalJSON struct { DisablePasteSummary apijson.Field Hook apijson.Field + SkipModelsFetch apijson.Field raw string ExtraFields map[string]apijson.Field } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 6c80f0b7c52..28f05bfe2ff 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1263,6 +1263,10 @@ export type Config = { * Tools that should only be available to primary agents. */ primary_tools?: Array + /** + * Skip automatic fetching of model information from models.dev on startup and timer + */ + skip_models_fetch?: boolean } } From ccdffcde7d3f393d9642b6c83fcbba79c532334b Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:16:41 -0500 Subject: [PATCH 02/20] refactor: try to make it a bit cleaner by inlining a called-only-once function Claude tried to add. --- packages/opencode/src/provider/models.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index c8bc72f4096..0b47728ed8c 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -64,21 +64,20 @@ export namespace ModelsDev { export type Provider = z.infer - async function setupTimer() { - if (timerSetup) return - timerSetup = true + export async function get() { const globalConfig = await Config.global() - if (!globalConfig.experimental?.skip_models_fetch) { + + // Setup timer only once, only if not skipping fetch + if (!timerSetup && !globalConfig.experimental?.skip_models_fetch) { + timerSetup = true setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() } - } - - export async function get() { - setupTimer() - const globalConfig = await Config.global() + + // Skip immediate refresh if configured to skip if (!globalConfig.experimental?.skip_models_fetch) { refresh() } + const file = Bun.file(filepath) const result = await file.json().catch(() => {}) if (result) return result as Record From e24b760ce3f21c300b720b5320f0730b88e48fde Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:23:16 -0500 Subject: [PATCH 03/20] refactor: these tests are probably overkill. --- packages/opencode/test/config/config.test.ts | 549 ------------------- 1 file changed, 549 deletions(-) delete mode 100644 packages/opencode/test/config/config.test.ts diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts deleted file mode 100644 index a7dbde887a9..00000000000 --- a/packages/opencode/test/config/config.test.ts +++ /dev/null @@ -1,549 +0,0 @@ -import { test, expect } from "bun:test" -import { Config } from "../../src/config/config" -import { Instance } from "../../src/project/instance" -import { tmpdir } from "../fixture/fixture" -import path from "path" -import fs from "fs/promises" -import { pathToFileURL } from "url" - -test("loads config with defaults when no files exist", async () => { - await using tmp = await tmpdir() - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.username).toBeDefined() - }, - }) -}) - -test("loads JSON config file", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - model: "test/model", - username: "testuser", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.model).toBe("test/model") - expect(config.username).toBe("testuser") - }, - }) -}) - -test("loads JSONC config file", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.jsonc"), - `{ - // This is a comment - "$schema": "https://opencode.ai/config.json", - "model": "test/model", - "username": "testuser" - }`, - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.model).toBe("test/model") - expect(config.username).toBe("testuser") - }, - }) -}) - -test("merges multiple config files with correct precedence", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.jsonc"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - model: "base", - username: "base", - }), - ) - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - model: "override", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.model).toBe("override") - expect(config.username).toBe("base") - }, - }) -}) - -test("handles environment variable substitution", async () => { - const originalEnv = process.env["TEST_VAR"] - process.env["TEST_VAR"] = "test_theme" - - try { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - theme: "{env:TEST_VAR}", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.theme).toBe("test_theme") - }, - }) - } finally { - if (originalEnv !== undefined) { - process.env["TEST_VAR"] = originalEnv - } else { - delete process.env["TEST_VAR"] - } - } -}) - -test("handles file inclusion substitution", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "included.txt"), "test_theme") - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - theme: "{file:included.txt}", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.theme).toBe("test_theme") - }, - }) -}) - -test("validates config schema and throws on invalid fields", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - invalid_field: "should cause error", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - // Strict schema should throw an error for invalid fields - await expect(Config.get()).rejects.toThrow() - }, - }) -}) - -test("throws error for invalid JSON", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), "{ invalid json }") - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await expect(Config.get()).rejects.toThrow() - }, - }) -}) - -test("handles agent configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - agent: { - test_agent: { - model: "test/model", - temperature: 0.7, - description: "test agent", - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test_agent"]).toEqual({ - model: "test/model", - temperature: 0.7, - description: "test agent", - }) - }, - }) -}) - -test("handles command configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - command: { - test_command: { - template: "test template", - description: "test command", - agent: "test_agent", - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.command?.["test_command"]).toEqual({ - template: "test template", - description: "test command", - agent: "test_agent", - }) - }, - }) -}) - -test("migrates autoshare to share field", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - autoshare: true, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.share).toBe("auto") - expect(config.autoshare).toBe(true) - }, - }) -}) - -test("migrates mode field to agent field", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - mode: { - test_mode: { - model: "test/model", - temperature: 0.5, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test_mode"]).toEqual({ - model: "test/model", - temperature: 0.5, - mode: "primary", - }) - }, - }) -}) - -test("loads config from .opencode directory", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - const opencodeDir = path.join(dir, ".opencode") - await fs.mkdir(opencodeDir, { recursive: true }) - const agentDir = path.join(opencodeDir, "agent") - await fs.mkdir(agentDir, { recursive: true }) - - await Bun.write( - path.join(agentDir, "test.md"), - `--- -model: test/model ---- -Test agent prompt`, - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.agent?.["test"]).toEqual({ - name: "test", - model: "test/model", - prompt: "Test agent prompt", - }) - }, - }) -}) - -test("updates config and writes to file", async () => { - await using tmp = await tmpdir() - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const newConfig = { model: "updated/model" } - await Config.update(newConfig as any) - - const writtenConfig = JSON.parse(await Bun.file(path.join(tmp.path, "config.json")).text()) - expect(writtenConfig.model).toBe("updated/model") - }, - }) -}) - -test("gets config directories", async () => { - await using tmp = await tmpdir() - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const dirs = await Config.directories() - expect(dirs.length).toBeGreaterThanOrEqual(1) - }, - }) -}) - -test("resolves scoped npm plugins in config", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - const pluginDir = path.join(dir, "node_modules", "@scope", "plugin") - await fs.mkdir(pluginDir, { recursive: true }) - - await Bun.write( - path.join(dir, "package.json"), - JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2), - ) - - await Bun.write( - path.join(pluginDir, "package.json"), - JSON.stringify( - { - name: "@scope/plugin", - version: "1.0.0", - type: "module", - main: "./index.js", - }, - null, - 2, - ), - ) - - await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n") - - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] }, null, 2), - ) - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - const pluginEntries = config.plugin ?? [] - - const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href - const expected = import.meta.resolve("@scope/plugin", baseUrl) - - expect(pluginEntries.includes(expected)).toBe(true) - - const scopedEntry = pluginEntries.find((entry) => entry === expected) - expect(scopedEntry).toBeDefined() - expect(scopedEntry?.includes("/node_modules/@scope/plugin/")).toBe(true) - }, - }) -}) - -test("merges plugin arrays from global and local configs", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - // Create a nested project structure with local .opencode config - const projectDir = path.join(dir, "project") - const opencodeDir = path.join(projectDir, ".opencode") - await fs.mkdir(opencodeDir, { recursive: true }) - - // Global config with plugins - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - plugin: ["global-plugin-1", "global-plugin-2"], - }), - ) - - // Local .opencode config with different plugins - await Bun.write( - path.join(opencodeDir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - plugin: ["local-plugin-1"], - }), - ) - }, - }) - - await Instance.provide({ - directory: path.join(tmp.path, "project"), - fn: async () => { - const config = await Config.get() - const plugins = config.plugin ?? [] - - // Should contain both global and local plugins - expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) - expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true) - expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) - - // Should have all 3 plugins (not replaced, but merged) - const pluginNames = plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")) - expect(pluginNames.length).toBeGreaterThanOrEqual(3) - }, - }) -}) - -test("deduplicates duplicate plugins from global and local configs", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - // Create a nested project structure with local .opencode config - const projectDir = path.join(dir, "project") - const opencodeDir = path.join(projectDir, ".opencode") - await fs.mkdir(opencodeDir, { recursive: true }) - - // Global config with plugins - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - plugin: ["duplicate-plugin", "global-plugin-1"], - }), - ) - - // Local .opencode config with some overlapping plugins - await Bun.write( - path.join(opencodeDir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - plugin: ["duplicate-plugin", "local-plugin-1"], - }), - ) - }, - }) - - await Instance.provide({ - directory: path.join(tmp.path, "project"), - fn: async () => { - const config = await Config.get() - const plugins = config.plugin ?? [] - - // Should contain all unique plugins - expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) - expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) - expect(plugins.some((p) => p.includes("duplicate-plugin"))).toBe(true) - - // Should deduplicate the duplicate plugin - const duplicatePlugins = plugins.filter((p) => p.includes("duplicate-plugin")) - expect(duplicatePlugins.length).toBe(1) - - // Should have exactly 3 unique plugins - const pluginNames = plugins.filter( - (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"), - ) - expect(pluginNames.length).toBe(3) - }, - }) -}) - -test("handles experimental.skip_models_fetch configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - experimental: { - skip_models_fetch: true, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.experimental?.skip_models_fetch).toBe(true) - }, - }) -}) - -test("handles experimental.skip_models_fetch set to false", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - experimental: { - skip_models_fetch: false, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.experimental?.skip_models_fetch).toBe(false) - }, - }) -}) From 1b4b91bcd9063ff207a087f5cdc7e82e2e85fcb7 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:24:47 -0500 Subject: [PATCH 04/20] fix: whoops, don't nuke that whole file, just resync it with dev\! --- packages/opencode/test/config/config.test.ts | 503 +++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 packages/opencode/test/config/config.test.ts diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts new file mode 100644 index 00000000000..2ff8c01cdb0 --- /dev/null +++ b/packages/opencode/test/config/config.test.ts @@ -0,0 +1,503 @@ +import { test, expect } from "bun:test" +import { Config } from "../../src/config/config" +import { Instance } from "../../src/project/instance" +import { tmpdir } from "../fixture/fixture" +import path from "path" +import fs from "fs/promises" +import { pathToFileURL } from "url" + +test("loads config with defaults when no files exist", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.username).toBeDefined() + }, + }) +}) + +test("loads JSON config file", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: "test/model", + username: "testuser", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.model).toBe("test/model") + expect(config.username).toBe("testuser") + }, + }) +}) + +test("loads JSONC config file", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.jsonc"), + `{ + // This is a comment + "$schema": "https://opencode.ai/config.json", + "model": "test/model", + "username": "testuser" + }`, + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.model).toBe("test/model") + expect(config.username).toBe("testuser") + }, + }) +}) + +test("merges multiple config files with correct precedence", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.jsonc"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: "base", + username: "base", + }), + ) + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + model: "override", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.model).toBe("override") + expect(config.username).toBe("base") + }, + }) +}) + +test("handles environment variable substitution", async () => { + const originalEnv = process.env["TEST_VAR"] + process.env["TEST_VAR"] = "test_theme" + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{env:TEST_VAR}", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("test_theme") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["TEST_VAR"] = originalEnv + } else { + delete process.env["TEST_VAR"] + } + } +}) + +test("handles file inclusion substitution", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "included.txt"), "test_theme") + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{file:included.txt}", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("test_theme") + }, + }) +}) + +test("validates config schema and throws on invalid fields", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + invalid_field: "should cause error", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // Strict schema should throw an error for invalid fields + await expect(Config.get()).rejects.toThrow() + }, + }) +}) + +test("throws error for invalid JSON", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), "{ invalid json }") + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await expect(Config.get()).rejects.toThrow() + }, + }) +}) + +test("handles agent configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + agent: { + test_agent: { + model: "test/model", + temperature: 0.7, + description: "test agent", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test_agent"]).toEqual({ + model: "test/model", + temperature: 0.7, + description: "test agent", + }) + }, + }) +}) + +test("handles command configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + command: { + test_command: { + template: "test template", + description: "test command", + agent: "test_agent", + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.command?.["test_command"]).toEqual({ + template: "test template", + description: "test command", + agent: "test_agent", + }) + }, + }) +}) + +test("migrates autoshare to share field", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + autoshare: true, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.share).toBe("auto") + expect(config.autoshare).toBe(true) + }, + }) +}) + +test("migrates mode field to agent field", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + mode: { + test_mode: { + model: "test/model", + temperature: 0.5, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test_mode"]).toEqual({ + model: "test/model", + temperature: 0.5, + mode: "primary", + }) + }, + }) +}) + +test("loads config from .opencode directory", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + const agentDir = path.join(opencodeDir, "agent") + await fs.mkdir(agentDir, { recursive: true }) + + await Bun.write( + path.join(agentDir, "test.md"), + `--- +model: test/model +--- +Test agent prompt`, + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.agent?.["test"]).toEqual({ + name: "test", + model: "test/model", + prompt: "Test agent prompt", + }) + }, + }) +}) + +test("updates config and writes to file", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const newConfig = { model: "updated/model" } + await Config.update(newConfig as any) + + const writtenConfig = JSON.parse(await Bun.file(path.join(tmp.path, "config.json")).text()) + expect(writtenConfig.model).toBe("updated/model") + }, + }) +}) + +test("gets config directories", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const dirs = await Config.directories() + expect(dirs.length).toBeGreaterThanOrEqual(1) + }, + }) +}) + +test("resolves scoped npm plugins in config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const pluginDir = path.join(dir, "node_modules", "@scope", "plugin") + await fs.mkdir(pluginDir, { recursive: true }) + + await Bun.write( + path.join(dir, "package.json"), + JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2), + ) + + await Bun.write( + path.join(pluginDir, "package.json"), + JSON.stringify( + { + name: "@scope/plugin", + version: "1.0.0", + type: "module", + main: "./index.js", + }, + null, + 2, + ), + ) + + await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n") + + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] }, null, 2), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + const pluginEntries = config.plugin ?? [] + + const baseUrl = pathToFileURL(path.join(tmp.path, "opencode.json")).href + const expected = import.meta.resolve("@scope/plugin", baseUrl) + + expect(pluginEntries.includes(expected)).toBe(true) + + const scopedEntry = pluginEntries.find((entry) => entry === expected) + expect(scopedEntry).toBeDefined() + expect(scopedEntry?.includes("/node_modules/@scope/plugin/")).toBe(true) + }, + }) +}) + +test("merges plugin arrays from global and local configs", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create a nested project structure with local .opencode config + const projectDir = path.join(dir, "project") + const opencodeDir = path.join(projectDir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Global config with plugins + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["global-plugin-1", "global-plugin-2"], + }), + ) + + // Local .opencode config with different plugins + await Bun.write( + path.join(opencodeDir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["local-plugin-1"], + }), + ) + }, + }) + + await Instance.provide({ + directory: path.join(tmp.path, "project"), + fn: async () => { + const config = await Config.get() + const plugins = config.plugin ?? [] + + // Should contain both global and local plugins + expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true) + expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) + + // Should have all 3 plugins (not replaced, but merged) + const pluginNames = plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")) + expect(pluginNames.length).toBeGreaterThanOrEqual(3) + }, + }) +}) + +test("deduplicates duplicate plugins from global and local configs", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create a nested project structure with local .opencode config + const projectDir = path.join(dir, "project") + const opencodeDir = path.join(projectDir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Global config with plugins + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["duplicate-plugin", "global-plugin-1"], + }), + ) + + // Local .opencode config with some overlapping plugins + await Bun.write( + path.join(opencodeDir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["duplicate-plugin", "local-plugin-1"], + }), + ) + }, + }) + + await Instance.provide({ + directory: path.join(tmp.path, "project"), + fn: async () => { + const config = await Config.get() + const plugins = config.plugin ?? [] + + // Should contain all unique plugins + expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("duplicate-plugin"))).toBe(true) + + // Should deduplicate the duplicate plugin + const duplicatePlugins = plugins.filter((p) => p.includes("duplicate-plugin")) + expect(duplicatePlugins.length).toBe(1) + + // Should have exactly 3 unique plugins + const pluginNames = plugins.filter( + (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"), + ) + expect(pluginNames.length).toBe(3) + }, + }) +}) From 6137fcd1f5857a7c10fa24764a4a1b4e177df2f5 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:33:42 -0500 Subject: [PATCH 05/20] refactor: some better variable names. --- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/provider/models.ts | 6 +-- packages/opencode/test/config/config.test.ts | 46 ++++++++++++++++++++ packages/sdk/go/config.go | 4 +- packages/sdk/js/src/gen/types.gen.ts | 2 +- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 48cba0a5427..848f288687d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -674,7 +674,7 @@ export namespace Config { .array(z.string()) .optional() .describe("Tools that should only be available to primary agents."), - skip_models_fetch: z + skip_models_refresh: z .boolean() .optional() .describe("Skip automatic fetching of model information from models.dev on startup and timer"), diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 0b47728ed8c..98286821a51 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -67,14 +67,14 @@ export namespace ModelsDev { export async function get() { const globalConfig = await Config.global() - // Setup timer only once, only if not skipping fetch - if (!timerSetup && !globalConfig.experimental?.skip_models_fetch) { + // Setup timer only once, only if not skipping refresh + if (!timerSetup && !globalConfig.experimental?.skip_models_refresh) { timerSetup = true setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() } // Skip immediate refresh if configured to skip - if (!globalConfig.experimental?.skip_models_fetch) { + if (!globalConfig.experimental?.skip_models_refresh) { refresh() } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 2ff8c01cdb0..f05efb3cc5d 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -501,3 +501,49 @@ test("deduplicates duplicate plugins from global and local configs", async () => }, }) }) + +test("handles experimental.skip_models_refresh configuration", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + experimental: { + skip_models_refresh: true, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.experimental?.skip_models_refresh).toBe(true) + }, + }) +}) + +test("handles experimental.skip_models_refresh set to false", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + experimental: { + skip_models_refresh: false, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.experimental?.skip_models_refresh).toBe(false) + }, + }) +}) diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index 268dbcd68b4..9828435521e 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -705,7 +705,7 @@ func (r configCommandJSON) RawJSON() string { type ConfigExperimental struct { DisablePasteSummary bool `json:"disable_paste_summary"` Hook ConfigExperimentalHook `json:"hook"` - SkipModelsFetch bool `json:"skip_models_fetch"` + SkipModelsRefresh bool `json:"skip_models_refresh"` JSON configExperimentalJSON `json:"-"` } @@ -714,7 +714,7 @@ type ConfigExperimental struct { type configExperimentalJSON struct { DisablePasteSummary apijson.Field Hook apijson.Field - SkipModelsFetch apijson.Field + SkipModelsRefresh apijson.Field raw string ExtraFields map[string]apijson.Field } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 28f05bfe2ff..03ce487f0a4 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1266,7 +1266,7 @@ export type Config = { /** * Skip automatic fetching of model information from models.dev on startup and timer */ - skip_models_fetch?: boolean + skip_models_refresh?: boolean } } From 0acbbfe8b15bac4a078e2b6151c84407bf0e4b51 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:36:19 -0500 Subject: [PATCH 06/20] refacter: cleaner structure. --- packages/opencode/src/provider/models.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 98286821a51..ce6bd82f2f4 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -67,14 +67,15 @@ export namespace ModelsDev { export async function get() { const globalConfig = await Config.global() - // Setup timer only once, only if not skipping refresh - if (!timerSetup && !globalConfig.experimental?.skip_models_refresh) { - timerSetup = true - setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() - } - - // Skip immediate refresh if configured to skip + // Setup timer and immediate refresh only if not skipping refresh if (!globalConfig.experimental?.skip_models_refresh) { + // Setup timer only once + if (!timerSetup) { + timerSetup = true + setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref() + } + + // Trigger immediate refresh refresh() } From 886a46710327d43f138d193a52c65ffe6928896c Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 4 Dec 2025 03:37:21 -0500 Subject: [PATCH 07/20] tidy: revert needless change. --- packages/opencode/test/config/config.test.ts | 46 -------------------- 1 file changed, 46 deletions(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index f05efb3cc5d..2ff8c01cdb0 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -501,49 +501,3 @@ test("deduplicates duplicate plugins from global and local configs", async () => }, }) }) - -test("handles experimental.skip_models_refresh configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - experimental: { - skip_models_refresh: true, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.experimental?.skip_models_refresh).toBe(true) - }, - }) -}) - -test("handles experimental.skip_models_refresh set to false", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - experimental: { - skip_models_refresh: false, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const config = await Config.get() - expect(config.experimental?.skip_models_refresh).toBe(false) - }, - }) -}) From 0d0be0ddfb568d2e16282bb68b1b1277a9dc205e Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 8 Dec 2025 08:39:17 -0500 Subject: [PATCH 08/20] Fix TypeScript error: remove cacheKey from FileContents interface usage --- packages/desktop/src/pages/session.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 81f4dc1cbc4..1e86868fdcb 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -31,7 +31,7 @@ import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Terminal } from "@/components/terminal" -import { checksum } from "@opencode-ai/util/encode" + export default function Page() { const layout = useLayout() @@ -493,7 +493,6 @@ export default function Page() { file={{ name: f().path, contents: f().content?.content ?? "", - cacheKey: checksum(f().content?.content ?? ""), }} overflow="scroll" class="pb-40" From 5d46cd32111a07510d82e976c65bdb89d7c64aea Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 8 Dec 2025 13:36:09 -0500 Subject: [PATCH 09/20] fix: revert damaged file --- packages/desktop/src/pages/session.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 1e86868fdcb..81f4dc1cbc4 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -31,7 +31,7 @@ import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Terminal } from "@/components/terminal" - +import { checksum } from "@opencode-ai/util/encode" export default function Page() { const layout = useLayout() @@ -493,6 +493,7 @@ export default function Page() { file={{ name: f().path, contents: f().content?.content ?? "", + cacheKey: checksum(f().content?.content ?? ""), }} overflow="scroll" class="pb-40" From 49819d8bec62d1e9feee4ce47c622abd51ea4402 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 10 Dec 2025 15:11:06 -0500 Subject: [PATCH 10/20] Fix type error: useKittyKeyboard should be boolean --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 1107ddd6a55..1ca7e126985 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -144,7 +144,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise Date: Wed, 10 Dec 2025 20:11:05 -0500 Subject: [PATCH 11/20] fix: uncorrupt --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 1ca7e126985..1107ddd6a55 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -144,7 +144,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise Date: Wed, 31 Dec 2025 22:58:06 -0600 Subject: [PATCH 12/20] Reapply "fix(tui): don't show 'Agent not found' toast for subagents (#6528)" This reverts commit 97a0fd1d54414e5563d0b47a02666d1d044c2cac. --- .../opencode/src/cli/cmd/tui/component/prompt/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 2a0ac846165..ab9487e1dd4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -202,7 +202,11 @@ export function Prompt(props: PromptProps) { syncedSessionID = sessionID - if (msg.agent) local.agent.set(msg.agent) + // Only set agent if it's a primary agent (not a subagent) + const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent) + if (msg.agent && isPrimaryAgent) { + local.agent.set(msg.agent) + } if (msg.model) local.model.set(msg.model) if (msg.variant) local.model.variant.set(msg.variant) } From 19af938df9b59f563bad179c4c005555d1d9c7be Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Jan 2026 04:59:38 +0000 Subject: [PATCH 13/20] chore: generate --- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 73e83fe0b2f..4ed3e3f71f4 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5d9c660cebf..e1a3ad73f9d 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} \ No newline at end of file +} From 1b7ca13dd3edc57092365bd87d111be92d1cdd34 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 03:41:16 -0600 Subject: [PATCH 14/20] fix(app): update primitive colors --- packages/ui/src/styles/colors.css | 52 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/styles/colors.css b/packages/ui/src/styles/colors.css index d64fa925ba1..4e78f81da21 100644 --- a/packages/ui/src/styles/colors.css +++ b/packages/ui/src/styles/colors.css @@ -113,8 +113,8 @@ --cobalt-light-4: #daeaff; --cobalt-light-5: #c8e0ff; --cobalt-light-6: #b4d2ff; - --cobalt-dark-alpha-1: #0011f211; --cobalt-light-7: #98bfff; + --cobalt-dark-alpha-1: #0011f211; --cobalt-dark-alpha-2: #0048fe1c; --cobalt-dark-alpha-3: #004dff49; --cobalt-dark-alpha-4: #064dfd6b; @@ -125,8 +125,8 @@ --cobalt-dark-alpha-9: #034cff; --cobalt-dark-alpha-10: #003bffed; --cobalt-dark-alpha-11: #89b5ff; - --cobalt-dark-alpha-12: #cde2ff; --cobalt-light-8: #73a4ff; + --cobalt-dark-alpha-12: #cde2ff; --cobalt-light-9: #034cff; --cobalt-light-10: #0443de; --cobalt-light-11: #1251ec; @@ -541,4 +541,52 @@ --ink-light-alpha-10: #0004049c; --ink-light-alpha-11: #0007077e; --ink-light-alpha-12: #000202df; + --amber-light-1: #fefdfb; + --amber-light-2: #fff9ed; + --amber-light-3: #fff4d5; + --amber-light-4: #ffecbc; + --amber-light-5: #ffe3a2; + --amber-light-6: #ffd386; + --amber-light-7: #f3ba63; + --amber-light-8: #ee9d2b; + --amber-light-9: #ffb224; + --amber-light-10: #ffa01c; + --amber-light-11: #ad5700; + --amber-light-12: #4e2009; + --amber-dark-1: #1f1300; + --amber-dark-2: #271700; + --amber-dark-3: #341c00; + --amber-dark-4: #3f2200; + --amber-dark-5: #4a2900; + --amber-dark-6: #573300; + --amber-dark-7: #693f05; + --amber-dark-8: #824e00; + --amber-dark-9: #ffb224; + --amber-dark-10: #ffcb47; + --amber-dark-11: #f1a10d; + --amber-dark-12: #fef3dd; + --amber-lightalpha-1: #c0820505; + --amber-lightalpha-2: #ffab0211; + --amber-lightalpha-3: #ffbb012b; + --amber-lightalpha-4: #ffb70042; + --amber-lightalpha-5: #ffb3005e; + --amber-lightalpha-6: #ffa20177; + --amber-lightalpha-7: #ec8d009b; + --amber-lightalpha-8: #ea8900d3; + --amber-lightalpha-9: #ffa600db; + --amber-lightalpha-10: #ff9500e2; + --amber-lightalpha-11: #ab5300f9; + --amber-lightalpha-12: #481800f4; + --amber-darkalpha-1: #00000000; + --amber-darkalpha-2: #fd83000a; + --amber-darkalpha-3: #fe730016; + --amber-darkalpha-4: #ff7b0023; + --amber-darkalpha-5: #ff840030; + --amber-darkalpha-6: #ff95003f; + --amber-darkalpha-7: #ff970f54; + --amber-darkalpha-8: #ff990070; + --amber-darkalpha-9: #ffb625f9; + --amber-darkalpha-10: #ffce48f9; + --amber-darkalpha-11: #ffab0eef; + --amber-darkalpha-12: #fff8e1f9; } From a7e4539adc0500fcfcd1b504b7475cba70857c6c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:48:19 -0600 Subject: [PATCH 15/20] fix(app): update primitive colors --- packages/ui/src/styles/colors.css | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/styles/colors.css b/packages/ui/src/styles/colors.css index 4e78f81da21..61e3ac950ad 100644 --- a/packages/ui/src/styles/colors.css +++ b/packages/ui/src/styles/colors.css @@ -565,28 +565,28 @@ --amber-dark-10: #ffcb47; --amber-dark-11: #f1a10d; --amber-dark-12: #fef3dd; - --amber-lightalpha-1: #c0820505; - --amber-lightalpha-2: #ffab0211; - --amber-lightalpha-3: #ffbb012b; - --amber-lightalpha-4: #ffb70042; - --amber-lightalpha-5: #ffb3005e; - --amber-lightalpha-6: #ffa20177; - --amber-lightalpha-7: #ec8d009b; - --amber-lightalpha-8: #ea8900d3; - --amber-lightalpha-9: #ffa600db; - --amber-lightalpha-10: #ff9500e2; - --amber-lightalpha-11: #ab5300f9; - --amber-lightalpha-12: #481800f4; - --amber-darkalpha-1: #00000000; - --amber-darkalpha-2: #fd83000a; - --amber-darkalpha-3: #fe730016; - --amber-darkalpha-4: #ff7b0023; - --amber-darkalpha-5: #ff840030; - --amber-darkalpha-6: #ff95003f; - --amber-darkalpha-7: #ff970f54; - --amber-darkalpha-8: #ff990070; - --amber-darkalpha-9: #ffb625f9; - --amber-darkalpha-10: #ffce48f9; - --amber-darkalpha-11: #ffab0eef; - --amber-darkalpha-12: #fff8e1f9; + --amber-light-alpha-1: #c0820505; + --amber-light-alpha-2: #ffab0211; + --amber-light-alpha-3: #ffbb012b; + --amber-light-alpha-4: #ffb70042; + --amber-light-alpha-5: #ffb3005e; + --amber-light-alpha-6: #ffa20177; + --amber-light-alpha-7: #ec8d009b; + --amber-light-alpha-8: #ea8900d3; + --amber-light-alpha-9: #ffa600db; + --amber-light-alpha-10: #ff9500e2; + --amber-light-alpha-11: #ab5300f9; + --amber-light-alpha-12: #481800f4; + --amber-dark-alpha-1: #00000000; + --amber-dark-alpha-2: #fd83000a; + --amber-dark-alpha-3: #fe730016; + --amber-dark-alpha-4: #ff7b0023; + --amber-dark-alpha-5: #ff840030; + --amber-dark-alpha-6: #ff95003f; + --amber-dark-alpha-7: #ff970f54; + --amber-dark-alpha-8: #ff990070; + --amber-dark-alpha-9: #ffb625f9; + --amber-dark-alpha-10: #ffce48f9; + --amber-dark-alpha-11: #ffab0eef; + --amber-dark-alpha-12: #fff8e1f9; } From 788cec2c48958d33a7dded7ddbf7957cdea3b175 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 05:02:28 -0600 Subject: [PATCH 16/20] fix(util): checksum defensiveness --- packages/util/src/encode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/util/src/encode.ts b/packages/util/src/encode.ts index fc1f783bf27..bf6fa75dc28 100644 --- a/packages/util/src/encode.ts +++ b/packages/util/src/encode.ts @@ -20,6 +20,7 @@ export async function hash(content: string, algorithm = "SHA-256"): Promise Date: Thu, 1 Jan 2026 05:04:26 -0600 Subject: [PATCH 17/20] fix(util): checksum defensiveness --- packages/util/src/encode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/util/src/encode.ts b/packages/util/src/encode.ts index bf6fa75dc28..138cf16086d 100644 --- a/packages/util/src/encode.ts +++ b/packages/util/src/encode.ts @@ -19,8 +19,8 @@ export async function hash(content: string, algorithm = "SHA-256"): Promise Date: Thu, 1 Jan 2026 05:23:00 -0600 Subject: [PATCH 18/20] feat(app): context window window --- .../src/components/session-context-usage.tsx | 100 +++-- packages/app/src/context/layout.tsx | 62 ++- packages/app/src/pages/session.tsx | 397 +++++++++++++++++- 3 files changed, 502 insertions(+), 57 deletions(-) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index ece1f869573..53e578214b9 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -1,13 +1,25 @@ -import { createMemo, Show } from "solid-js" +import { Match, Show, Switch, createMemo } from "solid-js" import { Tooltip } from "@opencode-ai/ui/tooltip" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" -import { useSync } from "@/context/sync" +import { Button } from "@opencode-ai/ui/button" import { useParams } from "@solidjs/router" import { AssistantMessage } from "@opencode-ai/sdk/v2/client" -export function SessionContextUsage() { +import { useLayout } from "@/context/layout" +import { useSync } from "@/context/sync" + +interface SessionContextUsageProps { + variant?: "button" | "indicator" +} + +export function SessionContextUsage(props: SessionContextUsageProps) { const sync = useSync() const params = useParams() + const layout = useLayout() + + const variant = createMemo(() => props.variant ?? "button") + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey())) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const cost = createMemo(() => { @@ -19,7 +31,11 @@ export function SessionContextUsage() { }) const context = createMemo(() => { - const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage + const last = messages().findLast((x) => { + if (x.role !== "assistant") return false + const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write + return total > 0 + }) as AssistantMessage if (!last) return const total = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write @@ -30,33 +46,57 @@ export function SessionContextUsage() { } }) - return ( - - {(ctx) => ( - -
- {ctx().tokens} - Tokens -
-
- {ctx().percentage ?? 0}% - Usage -
-
- {cost()} - Cost -
+ const openContext = () => { + if (!params.id) return + layout.review.open() + tabs().open("context") + tabs().setActive("context") + } + + const circle = () => ( +
+ +
+ ) + + const tooltipValue = () => ( +
+ + {(ctx) => ( + <> +
+ {ctx().tokens} + Tokens +
+
+ {ctx().percentage ?? 0}% + Usage
- } - placement="top" - > -
- -
- - )} + + )} +
+
+ {cost()} + Cost +
+ +
Click to view context
+
+
+ ) + + return ( + + + + {circle()} + + + + + ) } diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 156adc4ffd2..613a0e0c172 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -209,38 +209,58 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, async open(tab: string) { const current = store.sessionTabs[sessionKey] ?? { all: [] } - if (tab !== "review") { - if (!current.all.includes(tab)) { - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: [tab], active: tab }) - } else { - setStore("sessionTabs", sessionKey, "all", [...current.all, tab]) - setStore("sessionTabs", sessionKey, "active", tab) - } + + if (tab === "review") { + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: [], active: tab }) return } + setStore("sessionTabs", sessionKey, "active", tab) + return } - if (!store.sessionTabs[sessionKey]) { - setStore("sessionTabs", sessionKey, { all: [], active: tab }) - } else { + + if (tab === "context") { + const all = [tab, ...current.all.filter((x) => x !== tab)] + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all, active: tab }) + return + } + setStore("sessionTabs", sessionKey, "all", all) setStore("sessionTabs", sessionKey, "active", tab) + return } + + if (!current.all.includes(tab)) { + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: [tab], active: tab }) + return + } + setStore("sessionTabs", sessionKey, "all", [...current.all, tab]) + setStore("sessionTabs", sessionKey, "active", tab) + return + } + + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: current.all, active: tab }) + return + } + setStore("sessionTabs", sessionKey, "active", tab) }, close(tab: string) { const current = store.sessionTabs[sessionKey] if (!current) return + + const all = current.all.filter((x) => x !== tab) batch(() => { - setStore( - "sessionTabs", - sessionKey, - "all", - current.all.filter((x) => x !== tab), - ) - if (current.active === tab) { - const index = current.all.findIndex((f) => f === tab) - const previous = current.all[Math.max(0, index - 1)] - setStore("sessionTabs", sessionKey, "active", previous) + setStore("sessionTabs", sessionKey, "all", all) + if (current.active !== tab) return + + const index = current.all.findIndex((f) => f === tab) + if (index <= 0) { + setStore("sessionTabs", sessionKey, "active", undefined) + return } + setStore("sessionTabs", sessionKey, "active", current.all[index - 1]) }) }, move(tab: string, to: number) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 24d7bb94f50..f738fec33e2 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -17,6 +17,7 @@ import { Dynamic } from "solid-js/web" import { useLocal, type LocalFile } from "@/context/local" import { createStore } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" +import { SessionContextUsage } from "@/components/session-context-usage" import { DateTime } from "luxon" import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -30,6 +31,10 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail" import { SessionReview } from "@opencode-ai/ui/session-review" +import { Markdown } from "@opencode-ai/ui/markdown" +import { Accordion } from "@opencode-ai/ui/accordion" +import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" +import { Code } from "@opencode-ai/ui/code" import { DragDropProvider, DragDropSensors, @@ -70,7 +75,7 @@ import { Select } from "@opencode-ai/ui/select" import { TextField } from "@opencode-ai/ui/text-field" import { base64Encode } from "@opencode-ai/util/encode" import { iife } from "@opencode-ai/util/iife" -import { Session } from "@opencode-ai/sdk/v2/client" +import { AssistantMessage, Session, type Message, type Part } from "@opencode-ai/sdk/v2/client" function same(a: readonly T[], b: readonly T[]) { if (a === b) return true @@ -817,7 +822,23 @@ export default function Page() { ) } - const showTabs = createMemo(() => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0)) + const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context")) + const openedTabs = createMemo(() => + tabs() + .all() + .filter((tab) => tab !== "context"), + ) + + const showTabs = createMemo( + () => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0 || contextOpen()), + ) + + const activeTab = createMemo(() => { + const active = tabs().active() + if (active) return active + if (diffs().length > 0) return "review" + return tabs().all()[0] ?? "review" + }) const mobileWorking = createMemo(() => status().type !== "idle") const mobileAutoScroll = createAutoScroll({ @@ -916,6 +937,347 @@ export default function Page() { ) + const ContextTab = () => { + const ctx = createMemo(() => { + const last = messages().findLast((x) => { + if (x.role !== "assistant") return false + const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write + return total > 0 + }) as AssistantMessage + if (!last) return + + const provider = sync.data.provider.all.find((x) => x.id === last.providerID) + const model = provider?.models[last.modelID] + const limit = model?.limit.context + + const input = last.tokens.input + const output = last.tokens.output + const reasoning = last.tokens.reasoning + const cacheRead = last.tokens.cache.read + const cacheWrite = last.tokens.cache.write + const total = input + output + reasoning + cacheRead + cacheWrite + const usage = limit ? Math.round((total / limit) * 100) : null + + return { + message: last, + provider, + model, + limit, + input, + output, + reasoning, + cacheRead, + cacheWrite, + total, + usage, + } + }) + + const cost = createMemo(() => { + const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(total) + }) + + const counts = createMemo(() => { + const all = messages() + const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0) + const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0) + return { + all: all.length, + user, + assistant, + } + }) + + const systemPrompt = createMemo(() => { + const msg = visibleUserMessages().findLast((m) => !!m.system) + const system = msg?.system + if (!system) return + const trimmed = system.trim() + if (!trimmed) return + return trimmed + }) + + const number = (value: number | null | undefined) => { + if (value === undefined) return "—" + if (value === null) return "—" + return value.toLocaleString() + } + + const percent = (value: number | null | undefined) => { + if (value === undefined) return "—" + if (value === null) return "—" + return value.toString() + "%" + } + + const time = (value: number | undefined) => { + if (!value) return "—" + return DateTime.fromMillis(value).toLocaleString(DateTime.DATETIME_MED) + } + + const providerLabel = createMemo(() => { + const c = ctx() + if (!c) return "—" + return c.provider?.name ?? c.message.providerID + }) + + const modelLabel = createMemo(() => { + const c = ctx() + if (!c) return "—" + if (c.model?.name) return c.model.name + return c.message.modelID + }) + + const breakdown = createMemo( + on( + () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()], + () => { + const c = ctx() + if (!c) return [] + const input = c.input + if (!input) return [] + + const out = { + system: systemPrompt()?.length ?? 0, + user: 0, + assistant: 0, + tool: 0, + } + + for (const msg of messages()) { + const parts = (sync.data.part[msg.id] ?? []) as Part[] + + if (msg.role === "user") { + for (const part of parts) { + if (part.type === "text") out.user += part.text.length + if (part.type === "file") out.user += part.source?.text.value.length ?? 0 + if (part.type === "agent") out.user += part.source?.value.length ?? 0 + } + continue + } + + if (msg.role === "assistant") { + for (const part of parts) { + if (part.type === "text") out.assistant += part.text.length + if (part.type === "reasoning") out.assistant += part.text.length + if (part.type === "tool") { + out.tool += Object.keys(part.state.input).length * 16 + if (part.state.status === "pending") out.tool += part.state.raw.length + if (part.state.status === "completed") out.tool += part.state.output.length + if (part.state.status === "error") out.tool += part.state.error.length + } + } + } + } + + const estimateTokens = (chars: number) => Math.ceil(chars / 4) + const system = estimateTokens(out.system) + const user = estimateTokens(out.user) + const assistant = estimateTokens(out.assistant) + const tool = estimateTokens(out.tool) + const estimated = system + user + assistant + tool + + const pct = (tokens: number) => (tokens / input) * 100 + const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%" + + const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => { + return [ + { + key: "system", + label: "System", + tokens: tokens.system, + width: pct(tokens.system), + percent: pctLabel(tokens.system), + color: "var(--syntax-info)", + }, + { + key: "user", + label: "User", + tokens: tokens.user, + width: pct(tokens.user), + percent: pctLabel(tokens.user), + color: "var(--syntax-success)", + }, + { + key: "assistant", + label: "Assistant", + tokens: tokens.assistant, + width: pct(tokens.assistant), + percent: pctLabel(tokens.assistant), + color: "var(--syntax-property)", + }, + { + key: "tool", + label: "Tool Calls", + tokens: tokens.tool, + width: pct(tokens.tool), + percent: pctLabel(tokens.tool), + color: "var(--syntax-warning)", + }, + { + key: "other", + label: "Other", + tokens: tokens.other, + width: pct(tokens.other), + percent: pctLabel(tokens.other), + color: "var(--syntax-comment)", + }, + ].filter((x) => x.tokens > 0) + } + + if (estimated <= input) { + return build({ system, user, assistant, tool, other: input - estimated }) + } + + const scale = input / estimated + const scaled = { + system: Math.floor(system * scale), + user: Math.floor(user * scale), + assistant: Math.floor(assistant * scale), + tool: Math.floor(tool * scale), + } + const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool + return build({ ...scaled, other: Math.max(0, input - scaledTotal) }) + }, + ), + ) + + function Stat(props: { label: string; value: JSX.Element }) { + return ( +
+
{props.label}
+
{props.value}
+
+ ) + } + + const stats = createMemo(() => { + const c = ctx() + const count = counts() + return [ + { label: "Session", value: info()?.title ?? params.id ?? "—" }, + { label: "Messages", value: count.all.toLocaleString() }, + { label: "Provider", value: providerLabel() }, + { label: "Model", value: modelLabel() }, + { label: "Context Limit", value: number(c?.limit) }, + { label: "Total Tokens", value: number(c?.total) }, + { label: "Usage", value: percent(c?.usage) }, + { label: "Input Tokens", value: number(c?.input) }, + { label: "Output Tokens", value: number(c?.output) }, + { label: "Reasoning Tokens", value: number(c?.reasoning) }, + { label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` }, + { label: "User Messages", value: count.user.toLocaleString() }, + { label: "Assistant Messages", value: count.assistant.toLocaleString() }, + { label: "Total Cost", value: cost() }, + { label: "Session Created", value: time(info()?.time.created) }, + { label: "Last Activity", value: time(c?.message.time.created) }, + ] satisfies { label: string; value: JSX.Element }[] + }) + + function RawMessageContent(props: { message: Message }) { + const file = createMemo(() => { + const parts = (sync.data.part[props.message.id] ?? []) as Part[] + const contents = JSON.stringify({ message: props.message, parts }, null, 2) + return { + name: `${props.message.role}-${props.message.id}.json`, + contents, + cacheKey: checksum(contents), + } + }) + + return + } + + function RawMessage(props: { message: Message }) { + return ( + + + +
+
+ {props.message.role} • {props.message.id} +
+
+
{time(props.message.time.created)}
+ +
+
+
+
+ +
+ +
+
+
+ ) + } + + return ( +
+
+
+ {(stat) => } +
+ + 0}> +
+
Context Breakdown
+
+ + {(segment) => ( +
+ )} + +
+
+ + {(segment) => ( +
+
+
{segment.label}
+
{segment.percent}
+
+ )} + +
+ +
+ + + + {(prompt) => ( +
+
System Prompt
+
+ +
+
+ )} +
+ +
+
Raw messages
+ + {(message) => } + +
+
+
+ ) + } + return (
@@ -1015,7 +1377,7 @@ export default function Page() { > - +
@@ -1035,8 +1397,24 @@ export default function Page() {
- - + + + tabs().close("context")} /> + + } + hideCloseButton + > +
+ +
Context
+
+
+
+ + {(tab) => } @@ -1072,7 +1450,14 @@ export default function Page() {
- + + +
+ +
+
+
+ {(tab) => { const [file] = createResource( () => tab, From c8b87eff40b70b279d1bfd2e7089666bddc69cb2 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 1 Jan 2026 11:25:32 +0000 Subject: [PATCH 19/20] release: v1.0.223 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index abf241f2407..26e9cd16818 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -98,7 +98,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -125,7 +125,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -149,7 +149,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -173,7 +173,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@opencode-ai/app": "workspace:*", "@solid-primitives/storage": "catalog:", @@ -201,7 +201,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -230,7 +230,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -246,7 +246,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.222", + "version": "1.0.223", "bin": { "opencode": "./bin/opencode", }, @@ -348,7 +348,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -368,7 +368,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.222", + "version": "1.0.223", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -379,7 +379,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -392,7 +392,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -430,7 +430,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "zod": "catalog:", }, @@ -441,7 +441,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index a21c9f19709..a75285db8e3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.0.222", + "version": "1.0.223", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index e2475cb1b19..f8f79046e7c 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.222", + "version": "1.0.223", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 76d08f9631d..98b10997799 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.222", + "version": "1.0.223", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 7794ae718a2..d83e0860245 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.222", + "version": "1.0.223", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index d0e2700d849..17152678a45 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.222", + "version": "1.0.223", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 172c555ad3c..cb91f0e22b2 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.0.222", + "version": "1.0.223", "type": "module", "scripts": { "typecheck": "tsgo -b", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 86a3a89785d..db5f3219b7f 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.222", + "version": "1.0.223", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 3c6982e6ca0..d5524639ecb 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.0.222" +version = "1.0.223" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.222/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.223/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.222/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.223/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.222/opencode-linux-arm64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.223/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.222/opencode-linux-x64.tar.gz" +archive = "https://github.com/sst/opencode/releases/download/v1.0.223/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.222/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.223/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index b1653cd76fc..3a1a792244f 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.222", + "version": "1.0.223", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3a09ebfcf82..fc87255db3e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.222", + "version": "1.0.223", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 4ed3e3f71f4..998477e7382 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.0.222", + "version": "1.0.223", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -24,4 +24,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e1a3ad73f9d..6f7ddaccab1 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.0.222", + "version": "1.0.223", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", @@ -29,4 +29,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index c5ca2eb36fc..c14563cce9a 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.222", + "version": "1.0.223", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 83ccc4edec9..8200cd2f911 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.222", + "version": "1.0.223", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 7fc471f425a..7f434301a69 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.222", + "version": "1.0.223", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 5c3bdd2453b..a856419f9b3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.222", + "version": "1.0.223", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index c93a1edde3d..1ead3d20eda 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.0.222", + "version": "1.0.223", "publisher": "sst-dev", "repository": { "type": "git", From ff3aa636c5d43fff32cf53a353581fd9e44fd991 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Jan 2026 12:04:57 +0000 Subject: [PATCH 20/20] ignore: update download stats 2026-01-01 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index db2e14f7a74..2d2b7c476cd 100644 --- a/STATS.md +++ b/STATS.md @@ -187,3 +187,4 @@ | 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) | | 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) | | 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) | +| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |