From 621c51b56ae77d841c3d767e15fdd11f08811e59 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Fri, 23 Oct 2026 12:16:51 +1300 Subject: [PATCH 1/3] fix: use current OpenCode theme tokens --- .changeset/2fead794.md | 5 ++ .changeset/616f2eea.md | 5 ++ bun.lock | 26 ++++---- package.json | 2 +- .../__tests__/hint.test.tsx | 27 ++++++--- packages/opencode-force-input/src/hint.ts | 34 ++++++++--- .../__tests__/components.test.tsx | 10 ++-- .../__tests__/plugin.test.tsx | 10 ++-- .../__tests__/production-plugin.test.tsx | 10 ++-- .../opencode-usage-limits/src/components.tsx | 60 ++++++++++++++----- 10 files changed, 128 insertions(+), 61 deletions(-) create mode 100644 .changeset/2fead794.md create mode 100644 .changeset/616f2eea.md diff --git a/.changeset/2fead794.md b/.changeset/2fead794.md new file mode 100644 index 0000000..7f4f67a --- /dev/null +++ b/.changeset/2fead794.md @@ -0,0 +1,5 @@ +--- +"@mynameistito/opencode-usage-limits": patch +--- + +Use OpenCode 2.1.3 theme tokens for usage colors diff --git a/.changeset/616f2eea.md b/.changeset/616f2eea.md new file mode 100644 index 0000000..94d654f --- /dev/null +++ b/.changeset/616f2eea.md @@ -0,0 +1,5 @@ +--- +"@mynameistito/opencode-force-input": patch +--- + +Use OpenCode 2.1.3 theme tokens in the prompt hint diff --git a/bun.lock b/bun.lock index 05dbabf..8a6f69d 100644 --- a/bun.lock +++ b/bun.lock @@ -44,7 +44,7 @@ }, "packages/opencode-force-input": { "name": "@mynameistito/opencode-force-input", - "version": "1.1.17", + "version": "1.1.18", "dependencies": { "@opencode/plugin": "catalog:", }, @@ -62,7 +62,7 @@ }, "packages/opencode-usage-limits": { "name": "@mynameistito/opencode-usage-limits", - "version": "1.3.6", + "version": "1.3.7", "dependencies": { "@opencode/plugin": "catalog:", "effect": "catalog:", @@ -143,7 +143,7 @@ "catalog": { "@effect/platform-bun": "4.0.0", "@effect/platform-node": "4.0.0", - "@opencode/plugin": "2.1.2", + "@opencode/plugin": "2.1.3", "@opentui/core": "0.6.6", "@opentui/solid": "0.6.6", "@types/node": "26.6.5", @@ -724,17 +724,17 @@ "@octokit/webhooks-methods": ["@octokit/webhooks-methods@6.0.0", "", {}, "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ=="], - "@opencode/ai": ["@opencode/ai@2.1.2", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.1.2", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.1", "google-auth-library": "10.5.0" } }, "sha512-I2LeTbMRDmVtSWaIF2B0mwQitEHHg89fL28LdWQVec6z9SeFrx7WpEry5rxvYGKPvut8Mz7Kz+XCnVX/xhhcKg=="], + "@opencode/ai": ["@opencode/ai@2.1.3", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.1.3", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.1", "google-auth-library": "10.5.0" } }, "sha512-RAfVsO0RoPrUlx8b5jeafPWPvFpM1oJBq1lXVo0JNyGjb9r6+IJhTQDNanQcADsFoMhnx5Kk7VWn6VUgfzOCEQ=="], - "@opencode/client": ["@opencode/client@2.1.2", "", { "dependencies": { "@opencode/protocol": "2.1.2", "@opencode/schema": "2.1.2" }, "peerDependencies": { "effect": "4.0.1", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-c7FqI/zMy6HrdVGHynzouiz3y1smokGuimh2+Is2iVWz8I8VzJEty9eTrvQkQzXG1Hj5K2FL1K5fytQ7yJEd9w=="], + "@opencode/client": ["@opencode/client@2.1.3", "", { "dependencies": { "@opencode/protocol": "2.1.3", "@opencode/schema": "2.1.3" }, "peerDependencies": { "effect": "4.0.1", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-7hj4Z4TtszUTmpM7BSIssAYMO3dfcu9c5mHwpC0nDGQImkdSo6mTcNnjtSt0I9v6yjiz3vuQNCOChg0z7Ccyyw=="], - "@opencode/plugin": ["@opencode/plugin@2.1.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.1.2", "@opencode/client": "2.1.2", "@opencode/protocol": "2.1.2", "@opencode/schema": "2.1.2", "@opencode/util": "2.1.2", "effect": "4.0.1" }, "peerDependencies": { "@opencode/theme": "2.1.2", "@opentui/core": ">=0.6.6", "@opentui/solid": ">=0.6.6", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-vgGoLF/Kb7uyOsRmAHViN7BRPrAl0c3UVqvKQ/ZDl42IKzqsxedpAyoVpwxQHt+3LF6non2sOvi6Y+kKzDHheg=="], + "@opencode/plugin": ["@opencode/plugin@2.1.3", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.1.3", "@opencode/client": "2.1.3", "@opencode/protocol": "2.1.3", "@opencode/schema": "2.1.3", "@opencode/util": "2.1.3", "effect": "4.0.1" }, "peerDependencies": { "@opencode/theme": "2.1.3", "@opentui/core": ">=0.6.6", "@opentui/solid": ">=0.6.6", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-b4TcGruVXrcmxQjdBieMCw0i+GOe/+0xUnNPLKbLOSq0PEwMjBsif/W73FsdZVo5O0LJq//s1+JbKhz8RpGSFA=="], - "@opencode/protocol": ["@opencode/protocol@2.1.2", "", { "dependencies": { "@opencode/schema": "2.1.2", "effect": "4.0.1" } }, "sha512-dNJuZrJlcUnc3mP7GFoawXECpUoUA+1TEbXvXkFKteq9tpZbO5Qsi1AWqocRJH2cIfG8v5rddyBTyX4+0ax9Yw=="], + "@opencode/protocol": ["@opencode/protocol@2.1.3", "", { "dependencies": { "@opencode/schema": "2.1.3", "effect": "4.0.1" } }, "sha512-OAaP6aaemBJVda3gYSgMPxmOmlMvlDtqCfUYtS1KN7pMAGb973qZ6a3u4ATg9LJuiIyR96ydBdbwB+c9JbB0gQ=="], - "@opencode/schema": ["@opencode/schema@2.1.2", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.1" } }, "sha512-gYbneOINd0Tt41krlC79OWjcXMPlW9EjIR+tmG3EYD8SEA4xCdZh0WhZLrFDlLMKDcb6d9xeHTFwhqGACt0IRg=="], + "@opencode/schema": ["@opencode/schema@2.1.3", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.1" } }, "sha512-TsAKqJvdyGgYR8XlbGBu2SAl23Czf92z1U2RD95O72lf78qUCBYbaHoN0gNZz5K6RdKiv7yI1h1zk2nk4nWSPw=="], - "@opencode/util": ["@opencode/util@2.1.2", "", { "dependencies": { "@effect/opentelemetry": "4.0.1", "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.1", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-9uyLAnOSgVRN6NLWEMcwXDmLl+n3FKbXxmAaZVC1l5zBPKEGvc9PKa8KG9Kar7n3iGSPRk6R/JhW/j9s0ciNEQ=="], + "@opencode/util": ["@opencode/util@2.1.3", "", { "dependencies": { "@effect/opentelemetry": "4.0.1", "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.1", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-aFygelb1I2CvtMQsYlPf0GqC8Z7KVv36J8TmAr2tqp2IA06k3umfYSncaNyvGClPTnHkUh/SrW6JxYonisNAFg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -1488,8 +1488,6 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="], - "alchemy": ["alchemy@2.0.0-beta.83", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.83", "@alchemy.run/floci": "2.0.0-beta.83", "@alchemy.run/node-utils": "2.0.0-beta.83", "@alchemy.run/sigil": "0.1.0-alpha.1", "@distilled.cloud/acme": "1.0.0-rc.15", "@distilled.cloud/aws": "1.0.0-rc.15", "@distilled.cloud/axiom": "1.0.0-rc.15", "@distilled.cloud/cloudflare": "1.0.0-rc.15", "@distilled.cloud/core": "1.0.0-rc.15", "@distilled.cloud/doppler": "1.0.0-rc.15", "@distilled.cloud/fly-io": "1.0.0-rc.15", "@distilled.cloud/gcp": "1.0.0-rc.15", "@distilled.cloud/hetzner": "1.0.0-rc.15", "@distilled.cloud/iceberg": "1.0.0-rc.15", "@distilled.cloud/infisical": "1.0.0-rc.15", "@distilled.cloud/neon": "1.0.0-rc.15", "@distilled.cloud/planetscale": "1.0.0-rc.15", "@distilled.cloud/prisma": "1.0.0-rc.15", "@distilled.cloud/railway": "1.0.0-rc.15", "@distilled.cloud/stripe": "1.0.0-rc.15", "@distilled.cloud/turso": "1.0.0-rc.15", "@distilled.cloud/zerossl": "1.0.0-rc.15", "@effect/sql-d1": "^4.0.0", "@effect/sql-sqlite-do": "^4.0.0", "@libsql/client": "^0.17.0", "@neon/functions": "0.11.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@types/aws-lambda": "^8.10.152", "capnweb": "^0.12.0", "fflate": "^0.8.3", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "rolldown": "1.2.5", "tinyglobby": "^0.2.17", "yaml": "^2.9.0" }, "peerDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.83", "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": "^4.0.0", "@effect/platform-node": "^4.0.0", "@effect/sql-libsql": "^4.0.0", "@effect/sql-mysql2": "^4.0.0", "@effect/sql-pg": "^4.0.0", "@effect/vitest": "^4.0.0", "@prisma/orm-postgres": "^8.0.0-rc.11", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": "^4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.24.2", "pg": "^8.22.0", "prisma": "^8.0.0-rc.15", "vite": "^8.0.7", "ws": "^8.21.0" }, "optionalPeers": ["@alchemy.run/frontend-frameworks", "@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-libsql", "@effect/sql-mysql2", "@effect/sql-pg", "@effect/vitest", "@prisma/orm-postgres", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "prisma", "vite", "ws"], "bin": { "alchemy": "./bin/cli.js" } }, "sha512-t0N4FwLU1JxLnKZGdsBNdqiycWDM5HEYmmIguMImplE07Vh+FPpMR8ypk3K3TY5Tg0sFFppXFVwtwiDp6xlX9w=="], "am-i-vibing": ["am-i-vibing@0.5.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA=="], @@ -3496,8 +3494,6 @@ "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "ajv-i18n/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "alchemy/rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -3722,6 +3718,8 @@ "yaml-language-server/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "yaml-language-server/ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -3852,8 +3850,6 @@ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "ajv-i18n/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "alchemy/rolldown/@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "alchemy/rolldown/@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], diff --git a/package.json b/package.json index acb14c3..710be7c 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/node": "26.6.5", "@effect/platform-bun": "4.0.0", "@effect/platform-node": "4.0.0", - "@opencode/plugin": "2.1.2", + "@opencode/plugin": "2.1.3", "@opentui/core": "0.6.6", "@opentui/solid": "0.6.6", "@typescript/native": "npm:typescript@^7.0.2", diff --git a/packages/opencode-force-input/__tests__/hint.test.tsx b/packages/opencode-force-input/__tests__/hint.test.tsx index 5aa38f8..fe0c07a 100644 --- a/packages/opencode-force-input/__tests__/hint.test.tsx +++ b/packages/opencode-force-input/__tests__/hint.test.tsx @@ -18,6 +18,14 @@ type SessionExecutionEvent = (typeof SESSION_EXECUTION_EVENTS)[number]; type FiredEvent = SessionExecutionEvent | "session.retry.scheduled"; const v2Theme = { + text: { + action: { primary: { base: RGBA.fromInts(154, 176, 255) } }, + base: RGBA.fromInts(208, 208, 208), + muted: RGBA.fromInts(128, 128, 128), + }, +} as const; + +const previousV2Theme = { text: { action: { primary: { default: RGBA.fromInts(154, 176, 255) } }, default: RGBA.fromInts(208, 208, 208), @@ -133,11 +141,9 @@ describe("hint options and colors", () => { expect(hintEnabled({ hint: false })).toBeFalsy(); }); - it("resolves structured and legacy theme colors", () => { + it("resolves current, previous, and legacy theme colors", () => { const structured = resolveHintColors(v2Theme); - expect(structured.key).toBe(v2Theme.text.default); - expect(structured.label).toBe(v2Theme.text.subdued); - expect(structured.force).toBe(v2Theme.text.action.primary.default); + const previous = resolveHintColors(previousV2Theme); const legacy = { text: RGBA.fromInts(1, 2, 3), @@ -149,11 +155,16 @@ describe("hint options and colors", () => { const bareColor = RGBA.fromInts(9, 9, 9); const bare = resolveHintColors({ text: bareColor }); - expect([structured, flat, bare]).toStrictEqual([ + expect([structured, previous, flat, bare]).toStrictEqual([ + { + force: v2Theme.text.action.primary.base, + key: v2Theme.text.base, + label: v2Theme.text.muted, + }, { - force: v2Theme.text.action.primary.default, - key: v2Theme.text.default, - label: v2Theme.text.subdued, + force: previousV2Theme.text.action.primary.default, + key: previousV2Theme.text.default, + label: previousV2Theme.text.subdued, }, { force: legacy.warning, diff --git a/packages/opencode-force-input/src/hint.ts b/packages/opencode-force-input/src/hint.ts index 70dbc78..5ad6d75 100644 --- a/packages/opencode-force-input/src/hint.ts +++ b/packages/opencode-force-input/src/hint.ts @@ -30,7 +30,15 @@ export interface ForceHintData { }; } -interface V2Theme { +interface CurrentV2Theme { + readonly text: { + readonly base: RGBA; + readonly muted: RGBA; + readonly action: { readonly primary: { readonly base: RGBA } }; + }; +} + +interface PreviousV2Theme { readonly text: { readonly default: RGBA; readonly subdued: RGBA; @@ -45,10 +53,10 @@ interface LegacyTheme { } /** - * Theme shapes seen across OpenCode v2 builds: the structured v2 theme and the - * older flat one. Both are accepted so the hint renders on either host. + * Theme token shapes seen across OpenCode v2 builds, plus the older flat + * theme shape. Both are accepted so the hint renders across host versions. */ -export type ForceHintTheme = V2Theme | LegacyTheme; +export type ForceHintTheme = CurrentV2Theme | PreviousV2Theme | LegacyTheme; export interface HintColors { /** Accent color for the force-submit action. */ @@ -59,8 +67,12 @@ export interface HintColors { readonly label: RGBA; } -/** Distinguishes the structured v2 theme from the older flat one. */ -const isV2Theme = (theme: ForceHintTheme): theme is V2Theme => +/** Identifies the current structured theme tokens. */ +const isCurrentV2Theme = (theme: ForceHintTheme): theme is CurrentV2Theme => + "base" in theme.text; + +/** Identifies the earlier structured theme tokens. */ +const isPreviousV2Theme = (theme: ForceHintTheme): theme is PreviousV2Theme => "default" in theme.text; /** @@ -70,7 +82,15 @@ const isV2Theme = (theme: ForceHintTheme): theme is V2Theme => * @returns Colors for the force action, key label, and explanatory copy. */ export const resolveHintColors = (theme: ForceHintTheme): HintColors => { - if (isV2Theme(theme)) { + if (isCurrentV2Theme(theme)) { + return { + force: theme.text.action.primary.base, + key: theme.text.base, + label: theme.text.muted, + }; + } + + if (isPreviousV2Theme(theme)) { return { force: theme.text.action.primary.default, key: theme.text.default, diff --git a/packages/opencode-usage-limits/__tests__/components.test.tsx b/packages/opencode-usage-limits/__tests__/components.test.tsx index 0f69767..cb7e09c 100644 --- a/packages/opencode-usage-limits/__tests__/components.test.tsx +++ b/packages/opencode-usage-limits/__tests__/components.test.tsx @@ -22,13 +22,13 @@ const color = RGBA.fromValues(1, 2, 3, 255); const theme = { text: { - default: color, + base: color, feedback: { - error: { default: color }, - success: { default: color }, - warning: { default: color }, + error: { base: color }, + success: { base: color }, + warning: { base: color }, }, - subdued: color, + muted: color, }, }; diff --git a/packages/opencode-usage-limits/__tests__/plugin.test.tsx b/packages/opencode-usage-limits/__tests__/plugin.test.tsx index dec52c3..736e985 100644 --- a/packages/opencode-usage-limits/__tests__/plugin.test.tsx +++ b/packages/opencode-usage-limits/__tests__/plugin.test.tsx @@ -31,13 +31,13 @@ const color = RGBA.fromValues(1, 2, 3, 255); const theme: UsageTheme = { text: { - default: color, + base: color, feedback: { - error: { default: color }, - success: { default: color }, - warning: { default: color }, + error: { base: color }, + success: { base: color }, + warning: { base: color }, }, - subdued: color, + muted: color, }, }; diff --git a/packages/opencode-usage-limits/__tests__/production-plugin.test.tsx b/packages/opencode-usage-limits/__tests__/production-plugin.test.tsx index 6e85295..a5ebe22 100644 --- a/packages/opencode-usage-limits/__tests__/production-plugin.test.tsx +++ b/packages/opencode-usage-limits/__tests__/production-plugin.test.tsx @@ -16,13 +16,13 @@ const color = RGBA.fromValues(1, 2, 3, 255); const theme: UsageTheme = { text: { - default: color, + base: color, feedback: { - error: { default: color }, - success: { default: color }, - warning: { default: color }, + error: { base: color }, + success: { base: color }, + warning: { base: color }, }, - subdued: color, + muted: color, }, }; diff --git a/packages/opencode-usage-limits/src/components.tsx b/packages/opencode-usage-limits/src/components.tsx index 9793400..c64942e 100644 --- a/packages/opencode-usage-limits/src/components.tsx +++ b/packages/opencode-usage-limits/src/components.tsx @@ -19,7 +19,19 @@ import type { } from "@/types.ts"; import { quotaUsedPercent } from "@/usage.ts"; -interface V2Theme { +interface CurrentV2Theme { + readonly text: { + readonly base: RGBA; + readonly muted: RGBA; + readonly feedback: { + readonly warning: { readonly base: RGBA }; + readonly error: { readonly base: RGBA }; + readonly success: { readonly base: RGBA }; + }; + }; +} + +interface PreviousV2Theme { readonly text: { readonly default: RGBA; readonly subdued: RGBA; @@ -39,7 +51,7 @@ interface LegacyTheme { readonly success: RGBA; } -export type UsageTheme = V2Theme | LegacyTheme; +export type UsageTheme = CurrentV2Theme | PreviousV2Theme | LegacyTheme; interface ThemeColors { readonly text: RGBA; @@ -49,29 +61,47 @@ interface ThemeColors { readonly success: RGBA; } -/** Local boundary for v2 theme data while the v2 plugin package is unavailable. */ -const isV2Theme = (theme: UsageTheme): theme is V2Theme => +/** Identifies the current structured theme tokens. */ +const isCurrentV2Theme = (theme: UsageTheme): theme is CurrentV2Theme => + typeof theme.text === "object" && + theme.text !== null && + "base" in theme.text && + "feedback" in theme.text; + +/** Identifies the earlier structured theme tokens. */ +const isPreviousV2Theme = (theme: UsageTheme): theme is PreviousV2Theme => typeof theme.text === "object" && theme.text !== null && + "default" in theme.text && "feedback" in theme.text; const resolveTheme = (theme: UsageTheme): ThemeColors => { - if (!isV2Theme(theme)) { + if (isCurrentV2Theme(theme)) { + return { + error: theme.text.feedback.error.base, + subdued: theme.text.muted, + success: theme.text.feedback.success.base, + text: theme.text.base, + warning: theme.text.feedback.warning.base, + }; + } + + if (isPreviousV2Theme(theme)) { return { - error: theme.error, - subdued: theme.textMuted, - success: theme.success, - text: theme.text, - warning: theme.warning, + error: theme.text.feedback.error.default, + subdued: theme.text.subdued, + success: theme.text.feedback.success.default, + text: theme.text.default, + warning: theme.text.feedback.warning.default, }; } return { - error: theme.text.feedback.error.default, - subdued: theme.text.subdued, - success: theme.text.feedback.success.default, - text: theme.text.default, - warning: theme.text.feedback.warning.default, + error: theme.error, + subdued: theme.textMuted, + success: theme.success, + text: theme.text, + warning: theme.warning, }; }; From e44d565277c86a51ea55f823bfeca747fe1db32e Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Fri, 23 Oct 2026 12:20:28 +1300 Subject: [PATCH 2/3] test: cover previous usage theme tokens --- .../__tests__/components.test.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/opencode-usage-limits/__tests__/components.test.tsx b/packages/opencode-usage-limits/__tests__/components.test.tsx index cb7e09c..a9f7693 100644 --- a/packages/opencode-usage-limits/__tests__/components.test.tsx +++ b/packages/opencode-usage-limits/__tests__/components.test.tsx @@ -466,6 +466,35 @@ describe(UsageLimitsPanel, () => { } }); + it("renders footer usage with previous structured theme tokens", async () => { + const previousTheme = { + text: { + default: color, + feedback: { + error: { default: color }, + success: { default: color }, + warning: { default: color }, + }, + subdued: color, + }, + }; + + const setup = await testRender( + () => ( + + ), + { height: 4, width: 80 } + ); + + try { + await setup.flush(); + expect(setup.captureCharFrame()).toContain("5h"); + expect(setup.captureCharFrame()).toContain("42%"); + } finally { + setup.renderer.destroy(); + } + }); + it("renders footer balances without a percentage or progress bar", async () => { const setup = await testRender( () => ( From 08ea0dea232d165e661dd083e1e0ce0bdff1dcbd Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Fri, 23 Oct 2026 12:26:17 +1300 Subject: [PATCH 3/3] test: assert usage status colors --- .../__tests__/components.test.tsx | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/opencode-usage-limits/__tests__/components.test.tsx b/packages/opencode-usage-limits/__tests__/components.test.tsx index a9f7693..b9131cd 100644 --- a/packages/opencode-usage-limits/__tests__/components.test.tsx +++ b/packages/opencode-usage-limits/__tests__/components.test.tsx @@ -19,14 +19,17 @@ import { } from "@/usage.ts"; const color = RGBA.fromValues(1, 2, 3, 255); +const errorColor = RGBA.fromValues(220, 40, 40, 255); +const warningColor = RGBA.fromValues(220, 160, 40, 255); +const successColor = RGBA.fromValues(40, 180, 80, 255); const theme = { text: { base: color, feedback: { - error: { base: color }, - success: { base: color }, - warning: { base: color }, + error: { base: errorColor }, + success: { base: successColor }, + warning: { base: warningColor }, }, muted: color, }, @@ -521,4 +524,42 @@ describe(UsageLimitsPanel, () => { setup.renderer.destroy(); } }); + + it.each([ + { expectedColor: errorColor, usedPercent: 90 }, + { expectedColor: warningColor, usedPercent: 70 }, + { expectedColor: successColor, usedPercent: 42 }, + ])( + "uses the matching theme color at $usedPercent%", + async ({ usedPercent, expectedColor }) => { + const setup = await testRender( + () => ( + + ), + { height: 2, width: 80 } + ); + + try { + await setup.flush(); + + const bar = setup + .captureSpans() + .lines.flatMap((line) => line.spans) + .find((span) => span.text.startsWith("[")); + + expect(bar).toBeDefined(); + expect(bar?.fg.equals(expectedColor)).toBeTruthy(); + } finally { + setup.renderer.destroy(); + } + } + ); });