Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd921ad33c |
@@ -30,6 +30,27 @@
|
||||
"turbo": "2.10.2",
|
||||
},
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode-ai/ai",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
"aws4fetch": "1.0.20",
|
||||
"effect": "catalog:",
|
||||
"google-auth-library": "10.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.20",
|
||||
@@ -168,7 +189,6 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/console/app": {
|
||||
@@ -336,10 +356,10 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
@@ -562,26 +582,6 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode-ai/ai",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
"aws4fetch": "1.0.20",
|
||||
"effect": "catalog:",
|
||||
"google-auth-library": "10.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.17.20",
|
||||
@@ -621,10 +621,10 @@
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/cli": "workspace:*",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
@@ -885,8 +885,8 @@
|
||||
"dependencies": {
|
||||
"@fontsource/commit-mono": "5.2.5",
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -2168,6 +2168,8 @@
|
||||
|
||||
"@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="],
|
||||
|
||||
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
|
||||
|
||||
"@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"],
|
||||
|
||||
"@opencode-ai/cli": ["@opencode-ai/cli@workspace:packages/cli"],
|
||||
@@ -2206,8 +2208,6 @@
|
||||
|
||||
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
||||
|
||||
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||
|
||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||
@@ -2872,7 +2872,7 @@
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-M9rMkTar7JcRrvUHsK1271AuWDmrISIPQpQ4TSHmYZ4KMisGnMH0gfjCWnBwdndR7skvvp/UheHhZGvO3Cr8/g=="],
|
||||
|
||||
@@ -2946,7 +2946,7 @@
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.6.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-PFzBVEBP5k8R+mK/c+VAKmtpUTL+KzBIXWJ6oM0GWOb31K+QgymXV9IW03XLPM1wtkC7oAb9ZBN2aswSSVbNFg=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@smithy/util-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="],
|
||||
|
||||
@@ -6336,10 +6336,6 @@
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
@@ -6458,26 +6454,18 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/client-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="],
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="],
|
||||
@@ -6500,10 +6488,6 @@
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="],
|
||||
|
||||
"@aws-sdk/client-sts/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
@@ -6552,10 +6536,6 @@
|
||||
|
||||
"@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
|
||||
"@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
@@ -6850,6 +6830,10 @@
|
||||
|
||||
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
||||
|
||||
"@opencode-ai/console-app/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
|
||||
|
||||
"@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
||||
|
||||
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
|
||||
@@ -6864,10 +6848,6 @@
|
||||
|
||||
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode-ai/ai/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
|
||||
"@opencode-ai/ai/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||
@@ -8480,10 +8460,6 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
@@ -8990,12 +8966,8 @@
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
|
||||
@@ -9480,8 +9452,6 @@
|
||||
|
||||
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
|
||||
|
||||
+46
-25
@@ -46,6 +46,8 @@ import { EditorContextProvider } from "./context/editor"
|
||||
import { useEvent } from "./context/event"
|
||||
import { ClientProvider, useClient } from "./context/client"
|
||||
import { StartupLoading } from "./component/startup-loading"
|
||||
import { DevToolsSidebar } from "./component/devtools-sidebar"
|
||||
import { DevTools } from "./devtools"
|
||||
import { Reconnecting } from "./component/reconnecting"
|
||||
import { DataProvider, useData } from "./context/data"
|
||||
import { LocationProvider, useLocation } from "./context/location"
|
||||
@@ -86,6 +88,8 @@ import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-wi
|
||||
import { destroyRenderer } from "./util/renderer"
|
||||
import { cliErrorMessage, errorFormat } from "./util/error"
|
||||
|
||||
const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" })
|
||||
|
||||
registerOpencodeSpinner()
|
||||
|
||||
const appGlobalBindingCommands = [
|
||||
@@ -252,9 +256,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
const pluginRuntime = createPluginRuntime()
|
||||
|
||||
yield* Effect.tryPromise(async () => {
|
||||
const appStarted = performance.now()
|
||||
// Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
|
||||
void renderer.getPalette({ size: 16 }).catch(() => undefined)
|
||||
const modeStarted = performance.now()
|
||||
const mode = handoff?.mode ?? (await renderer.waitForThemeMode(1000)) ?? "dark"
|
||||
themePerformance.set("Detect light/dark mode", `${(performance.now() - modeStarted).toFixed(2)} ms`)
|
||||
if (renderer.isDestroyed) return
|
||||
|
||||
await render(() => {
|
||||
@@ -342,6 +349,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<EditorContextProvider>
|
||||
<PluginProvider packages={input.packages}>
|
||||
<App
|
||||
started={appStarted}
|
||||
pair={
|
||||
input.server.endpoint.auth
|
||||
? input.server.endpoint.auth
|
||||
@@ -398,10 +406,11 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
})
|
||||
})
|
||||
|
||||
function App(props: { pair?: DialogPairCredentials }) {
|
||||
function App(props: { pair?: DialogPairCredentials; started: number }) {
|
||||
const log = useLog({ component: "app" })
|
||||
const startup = useTuiStartup()
|
||||
const config = useConfig()
|
||||
const devtools = createMemo(() => config.data.debug?.devtools ?? false)
|
||||
const route = useRoute()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
@@ -421,6 +430,11 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
const plugins = usePlugin()
|
||||
const clipboard = useClipboard()
|
||||
|
||||
createEffect(() => {
|
||||
if (!themeState.ready) return
|
||||
themePerformance.set("Total", `${(performance.now() - props.started).toFixed(2)} ms`)
|
||||
})
|
||||
|
||||
// Toast once when an MCP server enters a failed or needs-auth state so the user knows to act,
|
||||
// without having to open the status panel. Tracking the last alerted status avoids re-toasting
|
||||
// the same problem on every refresh while still re-alerting if the state changes.
|
||||
@@ -1085,31 +1099,38 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
<Show when={Flag.OPENCODE_SHOW_TTFD}>
|
||||
<TimeToFirstDraw />
|
||||
</Show>
|
||||
<Show when={plugins.ready()}>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="column">
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Show when={route.data.type === "session" ? route.data.sessionID : undefined} keyed>
|
||||
{(_) => <Session />}
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={route.data.type === "plugin"}>
|
||||
<PluginRoute
|
||||
fallback={(id, name) => (
|
||||
<PluginRouteMissing id={id} name={name} onHome={() => route.navigate({ type: "home" })} />
|
||||
)}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="row">
|
||||
<box flexGrow={1} minWidth={0} flexDirection="column">
|
||||
<Show when={plugins.ready()}>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="column">
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Show when={route.data.type === "session" ? route.data.sessionID : undefined} keyed>
|
||||
{(_) => <Session />}
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={route.data.type === "plugin"}>
|
||||
<PluginRoute
|
||||
fallback={(id, name) => (
|
||||
<PluginRouteMissing id={id} name={name} onHome={() => route.navigate({ type: "home" })} />
|
||||
)}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
<box flexShrink={0}>
|
||||
<PluginSlot name="app.bottom" />
|
||||
</box>
|
||||
<PluginSlot name="app" />
|
||||
</Show>
|
||||
</box>
|
||||
<box flexShrink={0}>
|
||||
<PluginSlot name="app.bottom" />
|
||||
</box>
|
||||
<PluginSlot name="app" />
|
||||
</Show>
|
||||
<Show when={devtools()}>
|
||||
<DevToolsSidebar />
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={!startup.skipInitialLoading}>
|
||||
<StartupLoading ready={plugins.ready} />
|
||||
</Show>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { DevTools } from "../devtools"
|
||||
|
||||
export function DevToolsSidebar() {
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
|
||||
return (
|
||||
<box
|
||||
width={42}
|
||||
height="100%"
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
backgroundColor={themeV2.background()}
|
||||
>
|
||||
<For each={DevTools.data()}>
|
||||
{(group) => (
|
||||
<box flexShrink={0} marginBottom={1}>
|
||||
<box marginBottom={1}>
|
||||
<text fg={themeV2.background.action.primary()} attributes={TextAttributes.BOLD}>
|
||||
{group.title}
|
||||
</text>
|
||||
</box>
|
||||
<For each={group.entries}>
|
||||
{(entry) => (
|
||||
<box flexDirection="row">
|
||||
<text fg={themeV2.text.subdued()}>{entry.key}</text>
|
||||
<box flexGrow={1} />
|
||||
<text fg={themeV2.text()}>{String(entry.value)}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -206,6 +206,14 @@ const settings: Setting[] = [
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
{
|
||||
title: "DevTools",
|
||||
category: "Debug",
|
||||
path: ["debug", "devtools"],
|
||||
default: false,
|
||||
values: [false, true],
|
||||
labels: ["off", "on"],
|
||||
},
|
||||
]
|
||||
|
||||
export function DialogConfig() {
|
||||
|
||||
@@ -5,10 +5,10 @@ import { tint } from "../theme/color"
|
||||
import { logo } from "../logo"
|
||||
|
||||
export function Logo() {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
|
||||
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
||||
const shadow = tint(theme.background, fg, 0.25)
|
||||
const shadow = tint(themeV2.background(), fg, 0.25)
|
||||
const attrs = bold ? TextAttributes.BOLD : undefined
|
||||
return Array.from(line).map((char) => {
|
||||
if (char === "_") {
|
||||
@@ -52,8 +52,8 @@ export function Logo() {
|
||||
<For each={logo.left}>
|
||||
{(line, index) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
|
||||
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
|
||||
<box flexDirection="row">{renderLine(line, themeV2.text.subdued(), false)}</box>
|
||||
<box flexDirection="row">{renderLine(logo.right[index()], themeV2.text(), true)}</box>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -190,7 +190,7 @@ export function Prompt(props: PromptProps) {
|
||||
const renderer = useRenderer()
|
||||
const exit = useExit()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const animationsEnabled = createMemo(() => config.animations ?? true)
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
@@ -297,8 +297,8 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
if (props.disabled) input.cursorColor = theme.backgroundElement
|
||||
if (!props.disabled) input.cursorColor = theme.text
|
||||
if (props.disabled) input.cursorColor = themeV2.background.surface.offset()
|
||||
if (!props.disabled) input.cursorColor = themeV2.text()
|
||||
})
|
||||
|
||||
const usage = createMemo(() => {
|
||||
@@ -1306,10 +1306,10 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
|
||||
const highlight = createMemo(() => {
|
||||
if (leader()) return theme.border
|
||||
if (store.mode === "shell") return theme.primary
|
||||
if (leader()) return themeV2.border()
|
||||
if (store.mode === "shell") return themeV2.background.action.primary()
|
||||
const agent = local.agent.current()
|
||||
if (!agent) return theme.border
|
||||
if (!agent) return themeV2.border()
|
||||
return local.agent.color(agent.id)
|
||||
})
|
||||
|
||||
@@ -1326,7 +1326,7 @@ export function Prompt(props: PromptProps) {
|
||||
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
|
||||
animationsEnabled,
|
||||
)
|
||||
const borderHighlight = createMemo(() => tint(theme.border, highlight(), agentMetaAlpha()))
|
||||
const borderHighlight = createMemo(() => tint(themeV2.border(), highlight(), agentMetaAlpha()))
|
||||
|
||||
const placeholderText = createMemo(() => {
|
||||
if (props.showPlaceholder === false) return undefined
|
||||
@@ -1346,7 +1346,7 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
const spinnerDef = createMemo(() => {
|
||||
const agent = status() === "running" ? local.agent.current() : local.agent.current()
|
||||
const color = agent ? local.agent.color(agent.id) : theme.border
|
||||
const color = agent ? local.agent.color(agent.id) : themeV2.border()
|
||||
return {
|
||||
frames: createFrames({
|
||||
color,
|
||||
@@ -1383,16 +1383,16 @@ export function Prompt(props: PromptProps) {
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
backgroundColor={themeV2.background.action.secondary("focused")}
|
||||
flexGrow={1}
|
||||
width="100%"
|
||||
>
|
||||
<textarea
|
||||
width="100%"
|
||||
placeholder={placeholderText()}
|
||||
placeholderColor={theme.textMuted}
|
||||
textColor={leader() ? theme.textMuted : theme.text}
|
||||
focusedTextColor={leader() ? theme.textMuted : theme.text}
|
||||
placeholderColor={themeV2.text.subdued()}
|
||||
textColor={leader() ? themeV2.text.subdued() : themeV2.text()}
|
||||
focusedTextColor={leader() ? themeV2.text.subdued() : themeV2.text()}
|
||||
minHeight={1}
|
||||
maxHeight={maxHeight()}
|
||||
onContentChange={() => {
|
||||
@@ -1452,15 +1452,17 @@ export function Prompt(props: PromptProps) {
|
||||
setTimeout(() => {
|
||||
// setTimeout is a workaround and needs to be addressed properly
|
||||
if (!input || input.isDestroyed) return
|
||||
input.cursorColor = theme.text
|
||||
input.cursorColor = themeV2.text()
|
||||
}, 0)
|
||||
}}
|
||||
onMouseDown={(r: MouseEvent) => {
|
||||
if (props.disabled) return
|
||||
r.target?.focus()
|
||||
}}
|
||||
focusedBackgroundColor={theme.backgroundElement}
|
||||
cursorColor={props.disabled ? theme.backgroundElement : theme.text}
|
||||
focusedBackgroundColor={themeV2.background.action.secondary("focused")}
|
||||
cursorColor={
|
||||
props.disabled ? themeV2.background.surface.offset() : themeV2.text()
|
||||
}
|
||||
syntaxStyle={syntax()}
|
||||
/>
|
||||
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
|
||||
@@ -1472,22 +1474,32 @@ export function Prompt(props: PromptProps) {
|
||||
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().id)}
|
||||
</text>
|
||||
<Show when={store.mode === "normal" && local.permission.mode === "auto"}>
|
||||
<text fg={fadeColor(theme.textMuted, agentMetaAlpha())}>auto</text>
|
||||
<text fg={fadeColor(themeV2.text.subdued(), agentMetaAlpha())}>auto</text>
|
||||
</Show>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text>
|
||||
<text fg={fadeColor(themeV2.text.subdued(), modelMetaAlpha())}>·</text>
|
||||
<text
|
||||
flexShrink={0}
|
||||
fg={fadeColor(leader() ? theme.textMuted : theme.text, modelMetaAlpha())}
|
||||
fg={fadeColor(
|
||||
leader() ? themeV2.text.subdued() : themeV2.text(),
|
||||
modelMetaAlpha(),
|
||||
)}
|
||||
>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>{currentProviderLabel()}</text>
|
||||
<text fg={fadeColor(themeV2.text.subdued(), modelMetaAlpha())}>
|
||||
{currentProviderLabel()}
|
||||
</text>
|
||||
<Show when={showVariant()}>
|
||||
<text fg={fadeColor(theme.textMuted, variantMetaAlpha())}>·</text>
|
||||
<text fg={fadeColor(themeV2.text.subdued(), variantMetaAlpha())}>·</text>
|
||||
<text>
|
||||
<span style={{ fg: fadeColor(theme.warning, variantMetaAlpha()), bold: true }}>
|
||||
<span
|
||||
style={{
|
||||
fg: fadeColor(themeV2.text.feedback.warning(), variantMetaAlpha()),
|
||||
bold: true,
|
||||
}}
|
||||
>
|
||||
{local.model.variant.current()}
|
||||
</span>
|
||||
</text>
|
||||
@@ -1512,15 +1524,15 @@ export function Prompt(props: PromptProps) {
|
||||
borderColor={borderHighlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
||||
vertical: themeV2.background.action.secondary("focused").a !== 0 ? "╹" : " ",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
height={1}
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
borderColor={themeV2.background.action.secondary("focused")}
|
||||
customBorderChars={
|
||||
theme.backgroundElement.a !== 0
|
||||
themeV2.background.action.secondary("focused").a !== 0
|
||||
? {
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
@@ -1537,13 +1549,25 @@ export function Prompt(props: PromptProps) {
|
||||
<Match when={status() === "running"}>
|
||||
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
|
||||
<box marginLeft={1}>
|
||||
<Show when={config.animations ?? true} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||
<Show
|
||||
when={config.animations ?? true}
|
||||
fallback={<text fg={themeV2.text.subdued()}>[⋯]</text>}
|
||||
>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
</Show>
|
||||
</box>
|
||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||
<text
|
||||
fg={store.interrupt > 0 ? themeV2.background.action.primary() : themeV2.text()}
|
||||
>
|
||||
esc{" "}
|
||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||
<span
|
||||
style={{
|
||||
fg:
|
||||
store.interrupt > 0
|
||||
? themeV2.background.action.primary()
|
||||
: themeV2.text.subdued(),
|
||||
}}
|
||||
>
|
||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||
</span>
|
||||
</text>
|
||||
@@ -1552,16 +1576,16 @@ export function Prompt(props: PromptProps) {
|
||||
<Match when={move.progress()}>
|
||||
{(progress) => (
|
||||
<box paddingLeft={3}>
|
||||
<Spinner color={theme.accent}>
|
||||
<Spinner color={themeV2.hue().accent[300]}>
|
||||
{progress()}
|
||||
<span style={{ fg: theme.textMuted }}>{".".repeat(move.creatingDots())}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{".".repeat(move.creatingDots())}</span>
|
||||
</Spinner>
|
||||
</box>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={move.pendingNew()}>
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.accent}>(new working copy)</text>
|
||||
<text fg={themeV2.hue().accent[300]}>(new working copy)</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
@@ -1570,7 +1594,7 @@ export function Prompt(props: PromptProps) {
|
||||
fallback={props.hint ?? <text />}
|
||||
>
|
||||
{(location) => (
|
||||
<text fg={theme.textMuted} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
||||
<text fg={themeV2.text.subdued()} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
||||
{location()}
|
||||
</text>
|
||||
)}
|
||||
@@ -1580,47 +1604,55 @@ export function Prompt(props: PromptProps) {
|
||||
<box gap={2} flexDirection="row">
|
||||
<Show when={editorContextLabelState() !== "none" ? editorFileLabelDisplay() : undefined}>
|
||||
{(file) => (
|
||||
<text fg={editorContextLabelState() === "pending" ? theme.secondary : theme.textMuted}>{file()}</text>
|
||||
<text
|
||||
fg={
|
||||
editorContextLabelState() === "pending"
|
||||
? themeV2.hue().accent[500]
|
||||
: themeV2.text.subdued()
|
||||
}
|
||||
>
|
||||
{file()}
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<Switch>
|
||||
<Match when={liveWorkStatusVisible() || statusItems().length > 0}>
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
<text fg={themeV2.text.subdued()} wrapMode="none">
|
||||
<Show when={liveWorkStatusVisible() && liveWorkShortcut()}>
|
||||
{(shortcut) => <span style={{ fg: theme.text }}>{shortcut()} </span>}
|
||||
{(shortcut) => <span style={{ fg: themeV2.text() }}>{shortcut()} </span>}
|
||||
</Show>
|
||||
<Show when={subagentStatusLabel()}>
|
||||
{(label) => <span style={{ fg: theme.textMuted }}>{label()}</span>}
|
||||
{(label) => <span style={{ fg: themeV2.text.subdued() }}>{label()}</span>}
|
||||
</Show>
|
||||
<Show when={subagentStatusLabel() && shellStatusLabel()}>
|
||||
<span style={{ fg: theme.textMuted }}> · </span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · </span>
|
||||
</Show>
|
||||
<Show when={shellStatusLabel()}>
|
||||
{(label) => <span style={{ fg: theme.textMuted }}>{label()}</span>}
|
||||
{(label) => <span style={{ fg: themeV2.text.subdued() }}>{label()}</span>}
|
||||
</Show>
|
||||
<Show when={liveWorkStatusVisible() && statusItems().length > 0}>
|
||||
<span style={{ fg: theme.textMuted }}> · </span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · </span>
|
||||
</Show>
|
||||
<Show when={statusItems().length > 0}>
|
||||
<span style={{ fg: theme.textMuted }}>{statusItems().join(" · ")}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{statusItems().join(" · ")}</span>
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text}>
|
||||
{agentShortcut()} <span style={{ fg: theme.textMuted }}>agents</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{agentShortcut()} <span style={{ fg: themeV2.text.subdued() }}>agents</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<text fg={theme.text}>
|
||||
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{paletteShortcut()} <span style={{ fg: themeV2.text.subdued() }}>commands</span>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||
<text fg={themeV2.text()}>
|
||||
esc <span style={{ fg: themeV2.text.subdued() }}>exit shell mode</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -126,6 +126,11 @@ export const Info = Schema.Struct({
|
||||
onboarding: Schema.optional(Schema.Boolean).annotate({ description: "Show getting-started guidance" }),
|
||||
}),
|
||||
).annotate({ description: "In-product guidance settings" }),
|
||||
debug: Schema.optional(
|
||||
Schema.Struct({
|
||||
devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools sidebar" }),
|
||||
}),
|
||||
).annotate({ description: "Debugging settings" }),
|
||||
animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }),
|
||||
mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable terminal mouse capture" }),
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const data = useData()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme().theme
|
||||
const { theme, themeV2 } = useTheme()
|
||||
const route = useRoute()
|
||||
const paths = useTuiPaths()
|
||||
const args = useArgs()
|
||||
@@ -84,7 +84,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
current: undefined as string | undefined,
|
||||
})
|
||||
const colors = createMemo(() => [
|
||||
theme.secondary,
|
||||
themeV2.hue().accent[500],
|
||||
theme.accent,
|
||||
theme.success,
|
||||
theme.warning,
|
||||
|
||||
@@ -13,10 +13,14 @@ import {
|
||||
setSystemTheme,
|
||||
subscribeThemes,
|
||||
upsertTheme,
|
||||
type Theme,
|
||||
type ThemeJson,
|
||||
} from "../theme"
|
||||
import { generateSystem, terminalMode } from "../theme/system"
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createComponentTheme, type ComponentTheme } from "../theme/v2/component"
|
||||
import { resolveThemeFile } from "../theme/v2/resolve"
|
||||
import { migrateV1 } from "../theme/v2/v1-migrate"
|
||||
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useConfig } from "../config"
|
||||
@@ -24,6 +28,9 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { DevTools } from "../devtools"
|
||||
|
||||
const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" })
|
||||
|
||||
export type ThemeSource = Readonly<{
|
||||
discover(): Promise<Record<string, unknown>>
|
||||
@@ -77,6 +84,24 @@ type State = {
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
type ContextName = "elevated" | "overlay"
|
||||
type ThemeService = {
|
||||
theme: Theme
|
||||
themeV2: ComponentTheme
|
||||
contextual(context: ContextName): ThemeService
|
||||
readonly selected: string
|
||||
all: typeof allThemes
|
||||
has: typeof hasTheme
|
||||
syntax: Accessor<SyntaxStyle>
|
||||
mode: Accessor<"dark" | "light">
|
||||
locked: Accessor<boolean>
|
||||
lock(): void
|
||||
unlock(): void
|
||||
setMode(mode?: "dark" | "light", persist?: boolean): void
|
||||
set(theme: string): boolean
|
||||
readonly ready: boolean
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore<State>({
|
||||
themes: allThemes(),
|
||||
mode: "dark",
|
||||
@@ -141,6 +166,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
|
||||
onMount(() => {
|
||||
void Promise.allSettled([resolveSystemTheme(store.mode), syncCustomThemes()]).finally(() => {
|
||||
valuesV2()
|
||||
setStore("ready", true)
|
||||
})
|
||||
})
|
||||
@@ -149,6 +175,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
let systemThemeMode: "dark" | "light" | undefined
|
||||
let hasResolvedSystemTheme = false
|
||||
function resolveSystemTheme(mode: "dark" | "light" = store.mode) {
|
||||
const started = performance.now()
|
||||
return renderer
|
||||
.getPalette({ size: 16 })
|
||||
.then((colors: TerminalColors) => {
|
||||
@@ -172,6 +199,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
setSystemTheme(undefined)
|
||||
if (store.active === "system") setStore("active", "opencode")
|
||||
})
|
||||
.finally(() => themePerformance.set("Resolve system palette", duration(performance.now() - started)))
|
||||
}
|
||||
|
||||
let systemRefreshRunning = false
|
||||
@@ -257,23 +285,49 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
themeRefreshTimeouts.length = 0
|
||||
})
|
||||
|
||||
const values = createMemo(() => {
|
||||
const active = store.themes[store.active]
|
||||
if (active) return resolveTheme(active, store.mode)
|
||||
return resolveTheme(store.themes.opencode, store.mode)
|
||||
const source = createMemo(() => store.themes[store.active] ?? store.themes.opencode)
|
||||
const sourceName = createMemo(() => (store.themes[store.active] ? store.active : "opencode"))
|
||||
const values = createMemo(() => resolveTheme(source(), store.mode))
|
||||
const valuesV2 = createMemo(() => {
|
||||
const started = performance.now()
|
||||
const file = migrateV1(source())
|
||||
themePerformance.set("Convert V1 to V2", duration(performance.now() - started))
|
||||
const resolveStarted = performance.now()
|
||||
const result = resolveThemeFile(file, store.mode, sourceName())
|
||||
themePerformance.set("Resolve final theme", duration(performance.now() - resolveStarted))
|
||||
return result
|
||||
})
|
||||
const themeV2 = createComponentTheme(valuesV2)
|
||||
const contextsV2 = {
|
||||
elevated: createComponentTheme(() => {
|
||||
const theme = valuesV2().contexts["@context:elevated"]
|
||||
if (!theme) throw new Error("Theme context is not defined: elevated")
|
||||
return theme
|
||||
}),
|
||||
overlay: createComponentTheme(() => {
|
||||
const theme = valuesV2().contexts["@context:overlay"]
|
||||
if (!theme) throw new Error("Theme context is not defined: overlay")
|
||||
return theme
|
||||
}),
|
||||
}
|
||||
|
||||
createEffect(() => renderer.setBackgroundColor(values().background))
|
||||
|
||||
const syntax = createSyntaxStyleMemo(() => generateSyntax(values()))
|
||||
|
||||
return {
|
||||
theme: new Proxy(values(), {
|
||||
get(_target, prop) {
|
||||
// @ts-expect-error Properties are forwarded to the current reactive value.
|
||||
return values()[prop]
|
||||
},
|
||||
}),
|
||||
const theme = new Proxy(values(), {
|
||||
get(_target, prop) {
|
||||
// @ts-expect-error Properties are forwarded to the current reactive value.
|
||||
return values()[prop]
|
||||
},
|
||||
})
|
||||
function contextual(context: ContextName) {
|
||||
return contextualServices[context]
|
||||
}
|
||||
const service: ThemeService = {
|
||||
theme,
|
||||
themeV2,
|
||||
contextual,
|
||||
get selected() {
|
||||
return store.active
|
||||
},
|
||||
@@ -299,9 +353,18 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
return store.ready
|
||||
},
|
||||
}
|
||||
const contextualServices = {
|
||||
elevated: Object.assign(Object.create(service) as ThemeService, { themeV2: contextsV2.elevated }),
|
||||
overlay: Object.assign(Object.create(service) as ThemeService, { themeV2: contextsV2.overlay }),
|
||||
}
|
||||
return service
|
||||
},
|
||||
})
|
||||
|
||||
function duration(milliseconds: number) {
|
||||
return `${milliseconds.toFixed(2)} ms`
|
||||
}
|
||||
|
||||
export function createSyntaxStyleMemo(factory: () => SyntaxStyle) {
|
||||
const renderer = useRenderer()
|
||||
const retained = new Set<SyntaxStyle>()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export * as DevTools from "."
|
||||
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
export type Value = string | number | boolean | null
|
||||
|
||||
export type Group = Readonly<{
|
||||
id: string
|
||||
title: string
|
||||
entries: readonly Readonly<{ key: string; value: Value }>[]
|
||||
}>
|
||||
|
||||
const [groups, setGroups] = createSignal<readonly Group[]>([])
|
||||
|
||||
export function register(input: { id: string; title: string }) {
|
||||
setGroups((groups) => {
|
||||
if (groups.some((group) => group.id === input.id)) {
|
||||
return groups.map((group) => (group.id === input.id ? { ...group, title: input.title } : group))
|
||||
}
|
||||
return [...groups, { ...input, entries: [] }]
|
||||
})
|
||||
|
||||
return {
|
||||
set(key: string, value: Value) {
|
||||
setGroups((groups) =>
|
||||
groups.map((group) => {
|
||||
if (group.id !== input.id) return group
|
||||
if (group.entries.some((entry) => entry.key === key)) {
|
||||
return {
|
||||
...group,
|
||||
entries: group.entries.map((entry) => (entry.key === key ? { key, value } : entry)),
|
||||
}
|
||||
}
|
||||
return { ...group, entries: [...group.entries, { key, value }] }
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const data = groups
|
||||
@@ -8,7 +8,7 @@ import { abbreviateHome } from "../../runtime"
|
||||
import { FilePath } from "../../ui/file-path"
|
||||
|
||||
function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const paths = useTuiPaths()
|
||||
const directory = createMemo(() =>
|
||||
props.context.location ? abbreviateHome(props.context.location.directory, paths.home) : undefined,
|
||||
@@ -16,13 +16,13 @@ function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
{(value) => <FilePath value={value()} maxWidth={props.maxWidth} fg={theme.textMuted} />}
|
||||
{(value) => <FilePath value={value()} maxWidth={props.maxWidth} fg={themeV2.text.subdued()} />}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function Mcp(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const list = createMemo(() => props.context.data.location.mcp.server.list(props.context.location) ?? [])
|
||||
const failed = createMemo(() => list().some((item) => item.status.status === "failed"))
|
||||
const count = createMemo(() => list().filter((item) => item.status.status === "connected").length)
|
||||
@@ -30,25 +30,27 @@ function Mcp(props: { context: Plugin.Context }) {
|
||||
return (
|
||||
<Show when={list().length}>
|
||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
<Switch>
|
||||
<Match when={failed()}>
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
<span style={{ fg: themeV2.text.feedback.error() }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: count() > 0 ? theme.success : theme.textMuted }}>⊙ </span>
|
||||
<span style={{ fg: count() > 0 ? themeV2.text.feedback.success() : themeV2.text.subdued() }}>
|
||||
⊙{" "}
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{count()} MCP
|
||||
</text>
|
||||
<text fg={theme.textMuted}>/status</text>
|
||||
<text fg={themeV2.text.subdued()}>/status</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function View(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const mcpWidth = createMemo(() => {
|
||||
const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
|
||||
@@ -75,7 +77,7 @@ function View(props: { context: Plugin.Context }) {
|
||||
<Mcp context={props.context} />
|
||||
<box flexGrow={1} />
|
||||
<box flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{InstallationVersion}</text>
|
||||
<text fg={themeV2.text.subdued()}>{InstallationVersion}</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ export type ComposerProps = {
|
||||
}
|
||||
|
||||
export function Composer(props: ComposerProps) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
tabs: {} as Record<string, Tab>,
|
||||
@@ -111,8 +111,8 @@ export function Composer(props: ComposerProps) {
|
||||
<box
|
||||
{...SplitBorder}
|
||||
border={["left"]}
|
||||
borderColor={theme.border}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
borderColor={themeV2.border()}
|
||||
backgroundColor={themeV2.background()}
|
||||
paddingLeft={1}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
@@ -123,7 +123,7 @@ export function Composer(props: ComposerProps) {
|
||||
<Show
|
||||
when={tabList().length > 1}
|
||||
fallback={
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
|
||||
{tabList()[0]?.label ?? ""}
|
||||
</text>
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export function Composer(props: ComposerProps) {
|
||||
const isActive = createMemo(() => store.active === t.id)
|
||||
return (
|
||||
<text
|
||||
fg={isActive() ? theme.text : theme.textMuted}
|
||||
fg={isActive() ? themeV2.text() : themeV2.text.subdued()}
|
||||
attributes={isActive() ? TextAttributes.BOLD : undefined}
|
||||
>
|
||||
{t.label}
|
||||
@@ -144,7 +144,7 @@ export function Composer(props: ComposerProps) {
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.textMuted} onMouseUp={close}>
|
||||
<text fg={themeV2.text.subdued()} onMouseUp={close}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
@@ -154,19 +154,19 @@ export function Composer(props: ComposerProps) {
|
||||
<For each={footerHints()}>
|
||||
{(hint) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<span style={{ fg: themeV2.text() }}>
|
||||
<b>{hint.label}</b>{" "}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}>{hint.shortcut}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{hint.shortcut}</span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
<Show when={tabList().length > 1}>
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<span style={{ fg: themeV2.text() }}>
|
||||
<b>tabs</b>{" "}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}>←/→</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>←/→</span>
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TextAttributes, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { useData } from "../../../context/data"
|
||||
import { useLocation } from "../../../context/location"
|
||||
import { useClient } from "../../../context/client"
|
||||
import { useTheme, selectedForeground } from "../../../context/theme"
|
||||
import { useTheme } from "../../../context/theme"
|
||||
import { Keymap } from "../../../context/keymap"
|
||||
import { useComposerTab } from "./index"
|
||||
|
||||
@@ -12,8 +12,8 @@ export function ShellTab(props: { sessionID: string }) {
|
||||
const data = useData()
|
||||
const location = useLocation()
|
||||
const client = useClient()
|
||||
const { theme } = useTheme()
|
||||
const fg = selectedForeground(theme)
|
||||
const { themeV2 } = useTheme()
|
||||
const fg = themeV2.text.action.primary("focused")
|
||||
const composer = useComposerTab()
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
|
||||
@@ -96,8 +96,12 @@ export function ShellTab(props: { sessionID: string }) {
|
||||
|
||||
return (
|
||||
<Show when={composer.active("shell")}>
|
||||
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
|
||||
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No shell commands</text>}>
|
||||
<scrollbox
|
||||
scrollbarOptions={{ visible: false }}
|
||||
maxHeight={5}
|
||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||
>
|
||||
<Show when={entries().length > 0} fallback={<text fg={themeV2.text.subdued()}> No shell commands</text>}>
|
||||
<For each={entries()}>
|
||||
{(shell, index) => {
|
||||
const active = createMemo(() => index() === store.selected)
|
||||
@@ -106,11 +110,11 @@ export function ShellTab(props: { sessionID: string }) {
|
||||
flexDirection="row"
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||
backgroundColor={active() ? themeV2.background.action.primary() : RGBA.fromInts(0, 0, 0, 0)}
|
||||
onMouseOver={() => setStore("selected", index())}
|
||||
>
|
||||
<text
|
||||
fg={active() ? fg : theme.text}
|
||||
fg={active() ? fg : themeV2.text()}
|
||||
attributes={active() ? TextAttributes.BOLD : undefined}
|
||||
wrapMode="none"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TextAttributes, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { useRoute, useRouteData } from "../../../context/route"
|
||||
import { useData } from "../../../context/data"
|
||||
import { useClient } from "../../../context/client"
|
||||
import { useTheme, selectedForeground } from "../../../context/theme"
|
||||
import { useTheme } from "../../../context/theme"
|
||||
import { Locale } from "../../../util/locale"
|
||||
import { Keymap } from "../../../context/keymap"
|
||||
import { useComposerTab } from "./index"
|
||||
@@ -21,8 +21,8 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
const route = useRouteData("session")
|
||||
const data = useData()
|
||||
const client = useClient()
|
||||
const { theme } = useTheme()
|
||||
const fg = selectedForeground(theme)
|
||||
const { themeV2 } = useTheme()
|
||||
const fg = themeV2.text.action.primary("focused")
|
||||
const navigate = useRoute().navigate
|
||||
const composer = useComposerTab()
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
@@ -200,7 +200,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
maxHeight={5}
|
||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||
>
|
||||
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No subagents</text>}>
|
||||
<Show when={entries().length > 0} fallback={<text fg={themeV2.text.subdued()}> No subagents</text>}>
|
||||
<For each={entries()}>
|
||||
{(entry, index) => {
|
||||
const active = createMemo(() => index() === selected())
|
||||
@@ -213,7 +213,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
flexDirection="row"
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={active() ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||
backgroundColor={active() ? themeV2.background.action.primary() : RGBA.fromInts(0, 0, 0, 0)}
|
||||
onMouseOver={() => setStore("selected", index())}
|
||||
onMouseUp={() => {
|
||||
setStore("selected", index())
|
||||
@@ -222,7 +222,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
>
|
||||
<box flexGrow={1} minWidth={0} flexDirection="row">
|
||||
<text
|
||||
fg={active() ? fg : entry.current ? theme.primary : theme.text}
|
||||
fg={active() ? fg : entry.current ? themeV2.background.action.primary() : themeV2.text()}
|
||||
attributes={active() ? TextAttributes.BOLD : undefined}
|
||||
wrapMode="none"
|
||||
>
|
||||
@@ -230,7 +230,7 @@ export function SubagentsTab(props: { sessionID: string }) {
|
||||
</text>
|
||||
</box>
|
||||
<Show when={status()}>
|
||||
<text fg={active() ? fg : theme.textMuted} wrapMode="none">
|
||||
<text fg={active() ? fg : themeV2.text.subdued()} wrapMode="none">
|
||||
{status()}
|
||||
</text>
|
||||
</Show>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createStore } from "solid-js/store"
|
||||
import { useRoute } from "../../context/route"
|
||||
|
||||
export function Footer() {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const data = useData()
|
||||
const route = useRoute()
|
||||
const mcp = createMemo(
|
||||
@@ -54,35 +54,35 @@ export function Footer() {
|
||||
|
||||
return (
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{directory()}</text>
|
||||
<text fg={themeV2.text.subdued()}>{directory()}</text>
|
||||
<box gap={2} flexDirection="row" flexShrink={0}>
|
||||
<Switch>
|
||||
<Match when={store.welcome}>
|
||||
<text fg={theme.text}>
|
||||
Get started <span style={{ fg: theme.textMuted }}>/connect</span>
|
||||
<text fg={themeV2.text()}>
|
||||
Get started <span style={{ fg: themeV2.text.subdued() }}>/connect</span>
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={connected()}>
|
||||
<Show when={permissions().length > 0}>
|
||||
<text fg={theme.warning}>
|
||||
<span style={{ fg: theme.warning }}>△</span> {permissions().length} Permission
|
||||
<text fg={themeV2.text.feedback.warning()}>
|
||||
<span style={{ fg: themeV2.text.feedback.warning() }}>△</span> {permissions().length} Permission
|
||||
{permissions().length > 1 ? "s" : ""}
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={mcp()}>
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
<Switch>
|
||||
<Match when={mcpError()}>
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
<span style={{ fg: themeV2.text.feedback.error() }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: theme.success }}>⊙ </span>
|
||||
<span style={{ fg: themeV2.text.feedback.success() }}>⊙ </span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{mcp()} MCP
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>/status</text>
|
||||
<text fg={themeV2.text.subdued()}>/status</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
|
||||
@@ -3,8 +3,7 @@ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-j
|
||||
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core"
|
||||
import open from "open"
|
||||
import { selectedForeground, useTheme } from "../../context/theme"
|
||||
import { tint } from "../../theme/color"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import type { FormField, FormValue } from "@opencode-ai/client"
|
||||
import type { FormWithLocation } from "../../context/data"
|
||||
import { useClient } from "../../context/client"
|
||||
@@ -146,7 +145,7 @@ function requestOptions(form: FormWithLocation) {
|
||||
|
||||
export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
const client = useClient()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const keymap = Keymap.use()
|
||||
@@ -753,27 +752,27 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
|
||||
return (
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
border={["left"]}
|
||||
borderColor={theme.accent}
|
||||
borderColor={themeV2.hue().accent[300]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{props.form.title}</text>
|
||||
<text fg={themeV2.text.subdued()}>{props.form.title}</text>
|
||||
</box>
|
||||
<Show when={message()}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>{message()}</text>
|
||||
<text fg={themeV2.text()}>{message()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={!single() && !tabbed()}>
|
||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
{confirm() ? "Review" : `Field ${Math.min(store.tab, fields().length - 1) + 1} of ${fields().length}`}
|
||||
</text>
|
||||
<Show when={fields().length > 0}>
|
||||
<text fg={theme.textMuted}>
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
· {answered()}/{fields().length} completed
|
||||
</text>
|
||||
</Show>
|
||||
@@ -787,10 +786,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
const isAnswered = () => store.answers[item.key] !== undefined
|
||||
return (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
paddingRight={2}
|
||||
backgroundColor={
|
||||
isTab() ? theme.accent : tabHover() === index() ? theme.backgroundElement : theme.backgroundPanel
|
||||
isTab()
|
||||
? themeV2.background.formfield("selected")
|
||||
: tabHover() === index()
|
||||
? themeV2.background.formfield("focused")
|
||||
: themeV2.background()
|
||||
}
|
||||
onMouseOver={() => setTabHover(index())}
|
||||
onMouseOut={() => setTabHover(null)}
|
||||
@@ -801,7 +803,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
>
|
||||
<text
|
||||
fg={
|
||||
isTab() ? selectedForeground(theme, theme.accent) : isAnswered() ? theme.text : theme.textMuted
|
||||
isTab()
|
||||
? themeV2.text.formfield("selected")
|
||||
: tabHover() === index()
|
||||
? themeV2.text.formfield("focused")
|
||||
: isAnswered()
|
||||
? themeV2.text()
|
||||
: themeV2.text.subdued()
|
||||
}
|
||||
>
|
||||
{truncate(fieldLabel(item), 24)}
|
||||
@@ -811,10 +819,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
}}
|
||||
</For>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={
|
||||
confirm() ? theme.accent : tabHover() === "confirm" ? theme.backgroundElement : theme.backgroundPanel
|
||||
confirm()
|
||||
? themeV2.background.formfield("selected")
|
||||
: tabHover() === "confirm"
|
||||
? themeV2.background.formfield("focused")
|
||||
: themeV2.background()
|
||||
}
|
||||
onMouseOver={() => setTabHover("confirm")}
|
||||
onMouseOut={() => setTabHover(null)}
|
||||
@@ -823,7 +833,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
selectTabFromMouse()
|
||||
}}
|
||||
>
|
||||
<text fg={confirm() ? selectedForeground(theme, theme.accent) : theme.textMuted}>Confirm</text>
|
||||
<text fg={themeV2.text.formfield(confirm() ? "selected" : "default")}>Confirm</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -832,13 +842,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
{(external) => (
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<Show when={external().title}>
|
||||
<text fg={theme.text}>{external().title}</text>
|
||||
<text fg={themeV2.text()}>{external().title}</text>
|
||||
</Show>
|
||||
<Show when={external().description}>
|
||||
<text fg={theme.textMuted}>{external().description}</text>
|
||||
<text fg={themeV2.text.subdued()}>{external().description}</text>
|
||||
</Show>
|
||||
<text
|
||||
fg={theme.primary}
|
||||
fg={themeV2.background.action.primary()}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
openExternal()
|
||||
@@ -846,7 +856,13 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
>
|
||||
{external().url}
|
||||
</text>
|
||||
<text fg={store.answers[external().key] === true ? theme.success : theme.textMuted}>
|
||||
<text
|
||||
fg={
|
||||
store.answers[external().key] === true
|
||||
? themeV2.text.feedback.success()
|
||||
: themeV2.text.subdued()
|
||||
}
|
||||
>
|
||||
{store.answers[external().key] === true
|
||||
? "✓ Acknowledged"
|
||||
: store.externalReady[external().key]
|
||||
@@ -860,7 +876,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
<Show when={!confirm() && answerField()}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<box>
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
{answerField()!.description ?? fieldLabel(answerField()!)}
|
||||
{answerField()!.required ? " (required)" : ""}
|
||||
{multi() ? " (select all that apply)" : ""}
|
||||
@@ -879,12 +895,12 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
}}
|
||||
initialValue={input() || display(answerField()!, store.answers[answerField()!.key])}
|
||||
placeholder={placeholder()}
|
||||
placeholderColor={theme.textMuted}
|
||||
placeholderColor={themeV2.text.subdued()}
|
||||
minHeight={1}
|
||||
maxHeight={6}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
textColor={themeV2.text()}
|
||||
focusedTextColor={themeV2.text()}
|
||||
cursorColor={themeV2.text()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -908,23 +924,36 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box backgroundColor={active() ? theme.backgroundElement : undefined} paddingRight={1}>
|
||||
<text fg={active() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
|
||||
<box
|
||||
backgroundColor={
|
||||
active() ? themeV2.background.formfield("focused") : themeV2.background()
|
||||
}
|
||||
paddingRight={1}
|
||||
>
|
||||
<text fg={themeV2.text.formfield(active() ? "focused" : "default")}>
|
||||
{`${i() + 1}.`}
|
||||
</text>
|
||||
</box>
|
||||
<box backgroundColor={active() ? theme.backgroundElement : undefined}>
|
||||
<text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
|
||||
<box
|
||||
backgroundColor={
|
||||
active() ? themeV2.background.formfield("focused") : themeV2.background()
|
||||
}
|
||||
>
|
||||
<text
|
||||
fg={themeV2.text.formfield(
|
||||
active() ? "focused" : picked() ? "selected" : "default",
|
||||
)}
|
||||
>
|
||||
{multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
|
||||
</text>
|
||||
</box>
|
||||
<Show when={!multi()}>
|
||||
<text fg={theme.success}>{picked() ? " ✓" : ""}</text>
|
||||
<text fg={themeV2.text.formfield("selected")}>{picked() ? " ✓" : ""}</text>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={row.description}>
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.textMuted}>{row.description}</text>
|
||||
<text fg={themeV2.text.subdued()}>{row.description}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
@@ -941,18 +970,31 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined} paddingRight={1}>
|
||||
<text fg={other() ? tint(theme.textMuted, theme.secondary, 0.6) : theme.textMuted}>
|
||||
<box
|
||||
backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}
|
||||
paddingRight={1}
|
||||
>
|
||||
<text fg={themeV2.text.formfield(other() ? "focused" : "default")}>
|
||||
{`${rows().length + 1}.`}
|
||||
</text>
|
||||
</box>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
<box
|
||||
backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}
|
||||
>
|
||||
<text
|
||||
fg={
|
||||
other()
|
||||
? themeV2.text.formfield("focused")
|
||||
: customPicked()
|
||||
? themeV2.text.feedback.success()
|
||||
: themeV2.text()
|
||||
}
|
||||
>
|
||||
{multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"}
|
||||
</text>
|
||||
</box>
|
||||
<Show when={!multi()}>
|
||||
<text fg={theme.success}>{customPicked() ? " ✓" : ""}</text>
|
||||
<text fg={themeV2.text.feedback.success()}>{customPicked() ? " ✓" : ""}</text>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={store.editing}>
|
||||
@@ -968,18 +1010,18 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
}}
|
||||
initialValue={input()}
|
||||
placeholder="Type your own answer"
|
||||
placeholderColor={theme.textMuted}
|
||||
placeholderColor={themeV2.text.subdued()}
|
||||
minHeight={1}
|
||||
maxHeight={6}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
textColor={themeV2.text()}
|
||||
focusedTextColor={themeV2.text()}
|
||||
cursorColor={themeV2.text()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={!store.editing && input()}>
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.textMuted}>{input()}</text>
|
||||
<text fg={themeV2.text.subdued()}>{input()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
@@ -992,7 +1034,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
<Show when={confirm()}>
|
||||
<Show when={tabbed()}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>Review</text>
|
||||
<text fg={themeV2.text()}>Review</text>
|
||||
</box>
|
||||
</Show>
|
||||
<scrollbox
|
||||
@@ -1007,8 +1049,14 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
return (
|
||||
<box paddingLeft={1}>
|
||||
<text>
|
||||
<span style={{ fg: theme.textMuted }}>{truncate(fieldLabel(item), 40)}:</span>{" "}
|
||||
<span style={{ fg: acknowledged() ? theme.success : theme.error }}>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{truncate(fieldLabel(item), 40)}:</span>{" "}
|
||||
<span
|
||||
style={{
|
||||
fg: acknowledged()
|
||||
? themeV2.text.feedback.success()
|
||||
: themeV2.text.feedback.error(),
|
||||
}}
|
||||
>
|
||||
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
|
||||
</span>
|
||||
</text>
|
||||
@@ -1022,10 +1070,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
return (
|
||||
<box paddingLeft={1}>
|
||||
<text>
|
||||
<span style={{ fg: theme.textMuted }}>{truncate(fieldLabel(item), 40)}:</span>{" "}
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{truncate(fieldLabel(item), 40)}:</span>{" "}
|
||||
<span
|
||||
style={{
|
||||
fg: invalid() || missing() ? theme.error : answered() ? theme.text : theme.textMuted,
|
||||
fg:
|
||||
invalid() || missing()
|
||||
? themeV2.text.feedback.error()
|
||||
: answered()
|
||||
? themeV2.text()
|
||||
: themeV2.text.subdued(),
|
||||
}}
|
||||
>
|
||||
{invalid() ?? (answered() ? value() : missing() ? "(required)" : "(not answered)")}
|
||||
@@ -1049,41 +1102,41 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<Show when={!single()}>
|
||||
<text fg={theme.text}>
|
||||
{"⇆"} <span style={{ fg: theme.textMuted }}>tab</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{"⇆"} <span style={{ fg: themeV2.text.subdued() }}>tab</span>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={!confirm() && !textual() && !externalField()}>
|
||||
<text fg={theme.text}>
|
||||
{"↑↓"} <span style={{ fg: theme.textMuted }}>select</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{"↑↓"} <span style={{ fg: themeV2.text.subdued() }}>select</span>
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={confirm() && fields().length > 0}>
|
||||
<text fg={theme.text}>
|
||||
{"↑↓"} <span style={{ fg: theme.textMuted }}>scroll</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{"↑↓"} <span style={{ fg: themeV2.text.subdued() }}>scroll</span>
|
||||
</text>
|
||||
</Show>
|
||||
<text
|
||||
fg={theme.text}
|
||||
fg={themeV2.text()}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
if (confirm()) submit()
|
||||
if (externalField()) acknowledgeExternal()
|
||||
}}
|
||||
>
|
||||
enter <span style={{ fg: theme.textMuted }}>{actionLabel()}</span>
|
||||
enter <span style={{ fg: themeV2.text.subdued() }}>{actionLabel()}</span>
|
||||
</text>
|
||||
<Show when={externalField() && clipboard.write}>
|
||||
<text fg={theme.text} onMouseUp={copyExternal}>
|
||||
c <span style={{ fg: theme.textMuted }}>copy</span>
|
||||
<text fg={themeV2.text()} onMouseUp={copyExternal}>
|
||||
c <span style={{ fg: themeV2.text.subdued() }}>copy</span>
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text} onMouseUp={cancel}>
|
||||
esc <span style={{ fg: theme.textMuted }}>dismiss</span>
|
||||
<text fg={themeV2.text()} onMouseUp={cancel}>
|
||||
esc <span style={{ fg: themeV2.text.subdued() }}>dismiss</span>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={store.error}>
|
||||
<text fg={theme.error}>{store.error}</text>
|
||||
<text fg={themeV2.text.feedback.error()}>{store.error}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -108,7 +108,7 @@ export function Session() {
|
||||
const paths = useTuiPaths()
|
||||
const configState = useConfig()
|
||||
const config = configState.data
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const promptRef = usePromptRef()
|
||||
const session = createMemo(() => data.session.get(route.sessionID))
|
||||
const messages = () => data.session.message.list(route.sessionID)
|
||||
@@ -842,8 +842,8 @@ export function Session() {
|
||||
paddingLeft: 1,
|
||||
visible: showScrollbar(),
|
||||
trackOptions: {
|
||||
backgroundColor: theme.backgroundElement,
|
||||
foregroundColor: theme.border,
|
||||
backgroundColor: themeV2.background.action.secondary("focused"),
|
||||
foregroundColor: themeV2.border(),
|
||||
},
|
||||
}}
|
||||
stickyScroll={true}
|
||||
@@ -987,7 +987,7 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) =
|
||||
}
|
||||
|
||||
function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const shortcut = useCommandShortcut("session.background")
|
||||
const visible = createMemo(() => {
|
||||
const current = props.messages.findLast(
|
||||
@@ -1005,8 +1005,8 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
|
||||
<Show when={visible() && shortcut()}>
|
||||
{(value) => (
|
||||
<box marginTop={1} paddingLeft={3} flexShrink={0}>
|
||||
<text fg={theme.textMuted}>
|
||||
Press <span style={{ fg: theme.text }}>{value()}</span> to move running work to the background
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
Press <span style={{ fg: themeV2.text() }}>{value()}</span> to move running work to the background
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
@@ -1076,7 +1076,7 @@ function SessionReasoningGroupView(props: {
|
||||
message: (messageID: string) => SessionMessageInfo | undefined
|
||||
}) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [hover, setHover] = createSignal(false)
|
||||
@@ -1116,10 +1116,15 @@ function SessionReasoningGroupView(props: {
|
||||
icon={expanded() ? "-" : "+"}
|
||||
color={
|
||||
!props.completed
|
||||
? theme.text
|
||||
? themeV2.text()
|
||||
: hover() || expanded()
|
||||
? theme.warning
|
||||
: RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
|
||||
? themeV2.text.feedback.warning()
|
||||
: RGBA.fromValues(
|
||||
themeV2.text.feedback.warning().r,
|
||||
themeV2.text.feedback.warning().g,
|
||||
themeV2.text.feedback.warning().b,
|
||||
0.6,
|
||||
)
|
||||
}
|
||||
complete={props.completed}
|
||||
pending={latest() ? `Thinking: ${latest()}` : "Thinking"}
|
||||
@@ -1160,7 +1165,7 @@ function SessionReasoningGroupView(props: {
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundElement}
|
||||
borderColor={themeV2.background.action.secondary("focused")}
|
||||
paddingLeft={1}
|
||||
>
|
||||
<code
|
||||
@@ -1170,7 +1175,7 @@ function SessionReasoningGroupView(props: {
|
||||
syntaxStyle={syntax()}
|
||||
content={content()}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.textMuted}
|
||||
fg={themeV2.text.subdued()}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
@@ -1192,7 +1197,7 @@ function SessionGroupView(props: {
|
||||
completed: boolean
|
||||
message: (messageID: string) => SessionMessageInfo | undefined
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const ctx = use()
|
||||
const renderer = useRenderer()
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
@@ -1228,7 +1233,7 @@ function SessionGroupView(props: {
|
||||
<Show when={grouped().length > 0}>
|
||||
<InlineToolRow
|
||||
icon={props.completed ? "→" : "✱"}
|
||||
color={hover() ? theme.text : theme.textMuted}
|
||||
color={hover() ? themeV2.text() : themeV2.text.subdued()}
|
||||
complete={props.completed}
|
||||
pending={label()}
|
||||
spinner={!props.completed}
|
||||
@@ -1254,7 +1259,7 @@ function SessionGroupView(props: {
|
||||
function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
const ctx = use()
|
||||
const local = useLocal()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const model = createMemo(
|
||||
() =>
|
||||
ctx
|
||||
@@ -1274,25 +1279,25 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.error}
|
||||
borderColor={themeV2.text.feedback.error()}
|
||||
>
|
||||
<text fg={theme.textMuted}>{errorMessage(props.message.error)}</text>
|
||||
<text fg={themeV2.text.subdued()}>{errorMessage(props.message.error)}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<AssistantRetry retry={props.message.retry} />
|
||||
<box paddingLeft={3} marginTop={props.message.error && !interrupted() ? 1 : 0}>
|
||||
<text>
|
||||
<span style={{ fg: props.message.error ? theme.textMuted : local.agent.color(props.message.agent) }}>
|
||||
<span style={{ fg: props.message.error ? themeV2.text.subdued() : local.agent.color(props.message.agent) }}>
|
||||
{Locale.titlecase(props.message.agent)}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · {model()}</span>
|
||||
<Show when={duration()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
<Show when={interrupted()}>
|
||||
<span style={{ fg: theme.textMuted }}> · interrupted</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · interrupted</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
@@ -1302,7 +1307,7 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
|
||||
function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
|
||||
const ctx = use()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const text = () => {
|
||||
if (props.message.type === "agent-switched") return `Switched agent to ${props.message.agent}`
|
||||
if (props.message.type === "model-switched")
|
||||
@@ -1311,14 +1316,14 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
|
||||
}
|
||||
return (
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.textMuted}>{text()}</text>
|
||||
<text fg={themeV2.text.subdued()}>{text()}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
||||
const ctx = use()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const metadata = () => (props.message.type === "synthetic" ? props.message.metadata : undefined)
|
||||
const source = () => stringValue(metadata()?.source)
|
||||
const completion = () => source() === "subagent" || source() === "shell"
|
||||
@@ -1339,15 +1344,15 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
||||
const suffix = () =>
|
||||
Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - Bun.stringWidth(heading())))
|
||||
const color = () => {
|
||||
if (state() === "error") return theme.error
|
||||
if (state() === "cancelled") return theme.warning
|
||||
return theme.info
|
||||
if (state() === "error") return themeV2.text.feedback.error()
|
||||
if (state() === "cancelled") return themeV2.text.feedback.warning()
|
||||
return themeV2.text.feedback.info()
|
||||
}
|
||||
return (
|
||||
<Show
|
||||
when={completion()}
|
||||
fallback={
|
||||
<InlineToolRow icon="◈" color={theme.textMuted} pending="Notice" complete={true}>
|
||||
<InlineToolRow icon="◈" color={themeV2.text.subdued()} pending="Notice" complete={true}>
|
||||
{text()}
|
||||
</InlineToolRow>
|
||||
}
|
||||
@@ -1355,7 +1360,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
||||
<box marginLeft={3}>
|
||||
<text wrapMode="none">
|
||||
<span style={{ fg: color() }}>{heading()}</span>
|
||||
<span style={{ fg: theme.textMuted }}>{suffix()}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{suffix()}</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -1363,9 +1368,9 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
||||
}
|
||||
|
||||
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
return (
|
||||
<InlineToolRow icon="→" color={theme.textMuted} pending="Skill" complete={true}>
|
||||
<InlineToolRow icon="→" color={themeV2.text.subdued()} pending="Skill" complete={true}>
|
||||
Skill {props.message.name}
|
||||
</InlineToolRow>
|
||||
)
|
||||
@@ -1373,11 +1378,11 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
|
||||
|
||||
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const status = () => props.message.status
|
||||
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
|
||||
const content = createMemo(() => text().trim())
|
||||
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
|
||||
const color = () => (status() === "failed" ? themeV2.text.feedback.error() : themeV2.text.subdued())
|
||||
return (
|
||||
<box>
|
||||
<box flexDirection="row" alignItems="center">
|
||||
@@ -1406,8 +1411,8 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
||||
content={content()}
|
||||
tableOptions={{ style: "grid" }}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.markdownText}
|
||||
bg={theme.background}
|
||||
fg={themeV2.markdown()}
|
||||
bg={themeV2.background()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -1416,15 +1421,15 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
||||
}
|
||||
|
||||
function CompactionQueued() {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
return (
|
||||
<box flexDirection="row" alignItems="center">
|
||||
<box border={["top"]} borderColor={theme.border} flexGrow={1} />
|
||||
<box border={["top"]} borderColor={themeV2.border()} flexGrow={1} />
|
||||
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.textMuted}>◇</text>
|
||||
<text fg={theme.textMuted}>Compaction queued</text>
|
||||
<text fg={themeV2.text.subdued()}>◇</text>
|
||||
<text fg={themeV2.text.subdued()}>Compaction queued</text>
|
||||
</box>
|
||||
<box border={["top"]} borderColor={theme.border} flexGrow={1} />
|
||||
<box border={["top"]} borderColor={themeV2.border()} flexGrow={1} />
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -1445,7 +1450,7 @@ function RevertMessage(props: {
|
||||
}>
|
||||
}) {
|
||||
const ctx = use()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const route = useRouteData("session")
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
@@ -1470,15 +1475,15 @@ function RevertMessage(props: {
|
||||
marginTop={1}
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundPanel}
|
||||
borderColor={themeV2.background()}
|
||||
>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() ? themeV2.background.action.secondary("focused") : themeV2.background()}
|
||||
>
|
||||
<text fg={theme.textMuted}>
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
{props.count} message{props.count === 1 ? "" : "s"} reverted
|
||||
</text>
|
||||
<Show when={props.files.length > 0}>
|
||||
@@ -1486,7 +1491,7 @@ function RevertMessage(props: {
|
||||
<For each={props.files}>
|
||||
{(file) => (
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{statusLabel(file.status)}</text>
|
||||
<text fg={themeV2.text.subdued()}>{statusLabel(file.status)}</text>
|
||||
<FilePath
|
||||
value={file.file}
|
||||
maxWidth={Math.max(
|
||||
@@ -1496,21 +1501,21 @@ function RevertMessage(props: {
|
||||
(file.additions > 0 ? Bun.stringWidth(`+${file.additions}`) + 1 : 0) -
|
||||
(file.deletions > 0 ? Bun.stringWidth(`-${file.deletions}`) + 1 : 0),
|
||||
)}
|
||||
fg={theme.text}
|
||||
fg={themeV2.text()}
|
||||
/>
|
||||
<Show when={file.additions > 0}>
|
||||
<text fg={theme.diffAdded}>+{file.additions}</text>
|
||||
<text fg={themeV2.diff.text.added()}>+{file.additions}</text>
|
||||
</Show>
|
||||
<Show when={file.deletions > 0}>
|
||||
<text fg={theme.diffRemoved}>-{file.deletions}</text>
|
||||
<text fg={themeV2.diff.text.removed()}>-{file.deletions}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.text }}>{redoKey()}</span> or /redo to restore
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
<span style={{ fg: themeV2.text() }}>{redoKey()}</span> or /redo to restore
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
@@ -1518,7 +1523,7 @@ function RevertMessage(props: {
|
||||
}
|
||||
|
||||
function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "shell" }> }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
|
||||
|
||||
return (
|
||||
@@ -1528,13 +1533,13 @@ function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "she
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
gap={1}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.background}
|
||||
borderColor={themeV2.background()}
|
||||
>
|
||||
<text fg={theme.text}>$ {props.message.command}</text>
|
||||
<text fg={themeV2.text()}>$ {props.message.command}</text>
|
||||
<Show when={output()}>
|
||||
<text fg={theme.textMuted}>{output()}</text>
|
||||
<text fg={themeV2.text.subdued()}>{output()}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
@@ -1545,7 +1550,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
const data = useData()
|
||||
const local = useLocal()
|
||||
const files = createMemo(() => props.message.files ?? [])
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
|
||||
const queued = createMemo(
|
||||
@@ -1560,7 +1565,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
<box
|
||||
id={props.message.id}
|
||||
border={["left"]}
|
||||
borderColor={queued() ? theme.border : color()}
|
||||
borderColor={queued() ? themeV2.border() : color()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<box
|
||||
@@ -1583,19 +1588,27 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() ? themeV2.background.action.secondary("focused") : themeV2.background()}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.text}>{props.message.text}</text>
|
||||
<text fg={themeV2.text()}>{props.message.text}</text>
|
||||
<Show when={files().length}>
|
||||
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={files()}>
|
||||
{(file) => {
|
||||
const label = file.mime === "application/x-directory" ? "dir" : "file"
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: theme.secondary, fg: theme.background, bold: true }}>{` ${label} `}</span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}>
|
||||
<text fg={themeV2.text()}>
|
||||
<span
|
||||
style={{
|
||||
bg: themeV2.hue().accent[500],
|
||||
fg: themeV2.background(),
|
||||
bold: true,
|
||||
}}
|
||||
>
|
||||
{` ${label} `}
|
||||
</span>
|
||||
<span style={{ bg: themeV2.background.action.secondary("focused"), fg: themeV2.text.subdued() }}>
|
||||
{" "}
|
||||
{file.name ?? (file.source.type === "uri" ? file.source.uri : "attachment")}{" "}
|
||||
</span>
|
||||
@@ -1614,7 +1627,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||
function AssistantMessage(props: { message: SessionMessageAssistant; last: boolean }) {
|
||||
const ctx = use()
|
||||
const local = useLocal()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const model = createMemo(
|
||||
() =>
|
||||
ctx
|
||||
@@ -1700,11 +1713,11 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.error}
|
||||
borderColor={themeV2.text.feedback.error()}
|
||||
>
|
||||
<text fg={theme.textMuted}>{errorMessage(props.message.error)}</text>
|
||||
<text fg={themeV2.text.subdued()}>{errorMessage(props.message.error)}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<AssistantRetry retry={props.message.retry} />
|
||||
@@ -1712,12 +1725,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
|
||||
<Match when={props.last || final() || props.message.error}>
|
||||
<box paddingLeft={3}>
|
||||
<text>
|
||||
<span style={{ fg: props.message.error ? theme.textMuted : local.agent.color(props.message.agent) }}>
|
||||
<span style={{ fg: props.message.error ? themeV2.text.subdued() : local.agent.color(props.message.agent) }}>
|
||||
{Locale.titlecase(props.message.agent)}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · {model()}</span>
|
||||
<Show when={duration()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
@@ -1728,12 +1741,12 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
|
||||
}
|
||||
|
||||
function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
return (
|
||||
<Show when={props.retry}>
|
||||
{(retry) => (
|
||||
<box paddingLeft={3} marginTop={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
<text fg={themeV2.text.subdued()}>
|
||||
Retry attempt {retry().attempt} scheduled: {retry().error.message} [{retry().error.type}]
|
||||
</text>
|
||||
</box>
|
||||
@@ -1743,7 +1756,7 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
|
||||
}
|
||||
|
||||
function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; active: boolean }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const pathFormatter = usePathFormatter()
|
||||
const label = (part: SessionMessageAssistantTool) => {
|
||||
const input = typeof part.state.input === "string" ? {} : part.state.input
|
||||
@@ -1756,7 +1769,7 @@ function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; activ
|
||||
<box flexDirection="column">
|
||||
<InlineToolRow
|
||||
icon="✱"
|
||||
color={theme.textMuted}
|
||||
color={themeV2.text.subdued()}
|
||||
complete={!props.active}
|
||||
pending="Exploring"
|
||||
spinner={props.active}
|
||||
@@ -1766,7 +1779,7 @@ function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; activ
|
||||
<For each={props.parts}>
|
||||
{(part, index) => (
|
||||
<box paddingLeft={5}>
|
||||
<text fg={part.state.status === "error" ? theme.error : theme.textMuted}>
|
||||
<text fg={part.state.status === "error" ? themeV2.text.feedback.error() : themeV2.text.subdued()}>
|
||||
{index() === props.parts.length - 1 ? "└" : "├"} {label(part)}
|
||||
</text>
|
||||
</box>
|
||||
@@ -1783,7 +1796,7 @@ function ReasoningPart(props: {
|
||||
part: SessionMessageAssistantReasoning
|
||||
message: SessionMessageAssistant
|
||||
}) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const ctx = use()
|
||||
// Collapsed by default in hide mode: a single line throughout, so the
|
||||
// layout never shifts. Click to open the full markdown block, click to close.
|
||||
@@ -1811,7 +1824,7 @@ function ReasoningPart(props: {
|
||||
<box
|
||||
border={!inMinimal() || expanded() ? ["left"] : undefined}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundElement}
|
||||
borderColor={themeV2.background.action.secondary("focused")}
|
||||
paddingLeft={!inMinimal() || expanded() ? 1 : 0}
|
||||
>
|
||||
<box onMouseUp={toggle}>
|
||||
@@ -1829,7 +1842,7 @@ function ReasoningPart(props: {
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundElement}
|
||||
borderColor={themeV2.background.action.secondary("focused")}
|
||||
paddingLeft={inMinimal() ? 3 : 1}
|
||||
>
|
||||
<code
|
||||
@@ -1839,7 +1852,7 @@ function ReasoningPart(props: {
|
||||
syntaxStyle={syntax()}
|
||||
content={content()}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.textMuted}
|
||||
fg={themeV2.text.subdued()}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
@@ -1861,11 +1874,16 @@ function ReasoningHeader(props: {
|
||||
title: string | null
|
||||
duration?: string
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const fg = () =>
|
||||
props.open
|
||||
? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity)
|
||||
: theme.warning
|
||||
? RGBA.fromValues(
|
||||
themeV2.text.feedback.warning().r,
|
||||
themeV2.text.feedback.warning().g,
|
||||
themeV2.text.feedback.warning().b,
|
||||
0.6,
|
||||
)
|
||||
: themeV2.text.feedback.warning()
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@@ -1900,7 +1918,7 @@ function ReasoningHeader(props: {
|
||||
|
||||
function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box paddingLeft={3} flexShrink={0}>
|
||||
@@ -1911,8 +1929,8 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
|
||||
content={props.part.text.trim()}
|
||||
tableOptions={{ style: "grid" }}
|
||||
conceal={ctx.markdownMode() === "rendered"}
|
||||
fg={theme.markdownText}
|
||||
bg={theme.background}
|
||||
fg={themeV2.markdown()}
|
||||
bg={themeV2.background()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -2001,7 +2019,7 @@ type ToolProps = {
|
||||
part: SessionMessageAssistantTool
|
||||
}
|
||||
function GenericTool(props: ToolProps) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const output = createMemo(() => props.output?.trim() ?? "")
|
||||
const args = createMemo(() => JSON.stringify(props.input, null, 2))
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
@@ -2019,7 +2037,7 @@ function GenericTool(props: ToolProps) {
|
||||
<Show when={Object.keys(props.input).length > 0}>
|
||||
<box gap={1}>
|
||||
<text>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> Input </span>
|
||||
<span style={{ bg: themeV2.background.action.secondary("focused"), fg: themeV2.text.subdued() }}> Input </span>
|
||||
</text>
|
||||
<box paddingLeft={1}>
|
||||
<code
|
||||
@@ -2028,7 +2046,7 @@ function GenericTool(props: ToolProps) {
|
||||
syntaxStyle={syntax()}
|
||||
conceal={false}
|
||||
drawUnstyledText={false}
|
||||
fg={theme.text}
|
||||
fg={themeV2.text()}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
@@ -2037,10 +2055,10 @@ function GenericTool(props: ToolProps) {
|
||||
{(value) => (
|
||||
<box gap={1}>
|
||||
<text>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> Output </span>
|
||||
<span style={{ bg: themeV2.background.action.secondary("focused"), fg: themeV2.text.subdued() }}> Output </span>
|
||||
</text>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
<text fg={themeV2.text()} wrapMode="word">
|
||||
{value()}
|
||||
</text>
|
||||
</box>
|
||||
@@ -2066,7 +2084,7 @@ function InlineTool(props: {
|
||||
part: SessionMessageAssistantTool
|
||||
onClick?: () => void
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const ctx = use()
|
||||
const data = useData()
|
||||
const renderer = useRenderer()
|
||||
@@ -2092,10 +2110,10 @@ function InlineTool(props: {
|
||||
const clickable = createMemo(() => Boolean(props.onClick || failed()))
|
||||
const fg = createMemo(() => {
|
||||
if (props.color) return props.color
|
||||
if (permission()) return theme.warning
|
||||
if (failed()) return theme.error
|
||||
if (hover() && props.onClick) return theme.text
|
||||
return theme.textMuted
|
||||
if (permission()) return themeV2.text.feedback.warning()
|
||||
if (failed()) return themeV2.text.feedback.error()
|
||||
if (hover() && props.onClick) return themeV2.text()
|
||||
return themeV2.text.subdued()
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -2103,7 +2121,7 @@ function InlineTool(props: {
|
||||
icon={props.icon}
|
||||
iconColor={props.iconColor}
|
||||
color={fg()}
|
||||
errorColor={theme.error}
|
||||
errorColor={themeV2.text.feedback.error()}
|
||||
failed={failed()}
|
||||
denied={Boolean(denied())}
|
||||
error={error()}
|
||||
@@ -2225,9 +2243,9 @@ function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.El
|
||||
}
|
||||
|
||||
function StatusBadge(props: { children: string }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
return (
|
||||
<text flexShrink={0} bg={theme.backgroundElement} fg={theme.textMuted}>
|
||||
<text flexShrink={0} bg={themeV2.background.action.secondary("focused")} fg={themeV2.text.subdued()}>
|
||||
{" "}
|
||||
{props.children}{" "}
|
||||
</text>
|
||||
@@ -2242,7 +2260,7 @@ function BlockTool(props: {
|
||||
part?: SessionMessageAssistantTool
|
||||
spinner?: boolean
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const ctx = use()
|
||||
const data = useData()
|
||||
const renderer = useRenderer()
|
||||
@@ -2260,9 +2278,11 @@ function BlockTool(props: {
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
gap={1}
|
||||
backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
|
||||
backgroundColor={
|
||||
hover() ? themeV2.background.action.secondary() : themeV2.background()
|
||||
}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.background}
|
||||
borderColor={themeV2.background()}
|
||||
onMouseOver={() => props.onClick && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
@@ -2277,9 +2297,9 @@ function BlockTool(props: {
|
||||
{(title) => (
|
||||
<Show
|
||||
when={props.spinner}
|
||||
fallback={<text fg={permission() ? theme.warning : theme.textMuted}>{title()}</text>}
|
||||
fallback={<text fg={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}>{title()}</text>}
|
||||
>
|
||||
<Spinner color={permission() ? theme.warning : theme.textMuted}>{title().replace(/^# /, "")}</Spinner>
|
||||
<Spinner color={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}>{title().replace(/^# /, "")}</Spinner>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
@@ -2290,33 +2310,33 @@ function BlockTool(props: {
|
||||
<Show
|
||||
when={props.spinner}
|
||||
fallback={
|
||||
<text flexShrink={0} fg={permission() ? theme.warning : theme.textMuted}>
|
||||
<text flexShrink={0} fg={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}>
|
||||
{path().label}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={permission() ? theme.warning : theme.textMuted}>
|
||||
<Spinner color={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}>
|
||||
{path().label.replace(/^# /, "")}
|
||||
</Spinner>
|
||||
</Show>
|
||||
<FilePath
|
||||
value={path().value}
|
||||
maxWidth={Math.max(2, ctx.width - 4 - Bun.stringWidth(path().label) - (props.spinner ? 2 : 0))}
|
||||
fg={permission() ? theme.warning : theme.textMuted}
|
||||
fg={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}
|
||||
/>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
{props.children}
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
<text fg={themeV2.text.feedback.error()}>{error()}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function Shell(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const ctx = use()
|
||||
const client = useClient()
|
||||
const data = useData()
|
||||
@@ -2324,7 +2344,7 @@ function Shell(props: ToolProps) {
|
||||
const request = data.session.permission.list(ctx.sessionID)?.[0]
|
||||
return request?.source?.type === "tool" && request.source.callID === props.part.id
|
||||
})
|
||||
const color = createMemo(() => (permission() ? theme.warning : theme.text))
|
||||
const color = createMemo(() => (permission() ? themeV2.text.feedback.warning() : themeV2.text()))
|
||||
const shellID = createMemo(() => stringValue(props.metadata.shellID))
|
||||
const backgroundRunning = createMemo(() => {
|
||||
const id = shellID()
|
||||
@@ -2386,7 +2406,7 @@ function Shell(props: ToolProps) {
|
||||
isRunning() || props.part.state.status === "streaming" ? (
|
||||
<Spinner color={color()}>Writing command...</Spinner>
|
||||
) : (
|
||||
<text fg={theme.textMuted}>Writing command...</text>
|
||||
<text fg={themeV2.text.subdued()}>Writing command...</text>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -2394,14 +2414,14 @@ function Shell(props: ToolProps) {
|
||||
when={isRunning()}
|
||||
fallback={
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: theme.textMuted }}>{limited().slice(input().length)}</span>
|
||||
<span style={{ fg: themeV2.text() }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{limited().slice(input().length)}</span>
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={color()}>
|
||||
<span style={{ fg: theme.text }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: theme.textMuted }}>{limited().slice(input().length)}</span>
|
||||
<span style={{ fg: themeV2.text() }}>{limited().slice(0, input().length)}</span>
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{limited().slice(input().length)}</span>
|
||||
</Spinner>
|
||||
</Show>
|
||||
</Show>
|
||||
@@ -2414,7 +2434,7 @@ function Shell(props: ToolProps) {
|
||||
}
|
||||
|
||||
function Write(props: ToolProps) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const pathFormatter = usePathFormatter()
|
||||
const code = createMemo(() => {
|
||||
return stringValue(props.input.content) ?? ""
|
||||
@@ -2427,10 +2447,10 @@ function Write(props: ToolProps) {
|
||||
path={{ label: "# Wrote", value: pathFormatter.format(stringValue(props.input.path)) }}
|
||||
part={props.part}
|
||||
>
|
||||
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
|
||||
<line_number fg={themeV2.text.subdued()} minWidth={3} paddingRight={1}>
|
||||
<code
|
||||
conceal={false}
|
||||
fg={theme.text}
|
||||
fg={themeV2.text()}
|
||||
filetype={filetype(stringValue(props.input.path))}
|
||||
syntaxStyle={syntax()}
|
||||
content={code()}
|
||||
@@ -2462,7 +2482,7 @@ function Glob(props: ToolProps) {
|
||||
}
|
||||
|
||||
function Read(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const pathFormatter = usePathFormatter()
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const loaded = createMemo(() => {
|
||||
@@ -2485,7 +2505,7 @@ function Read(props: ToolProps) {
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
<text paddingLeft={3} fg={themeV2.text.subdued()}>
|
||||
↳ Loaded {pathFormatter.format(filepath)}
|
||||
</text>
|
||||
</box>
|
||||
@@ -2579,7 +2599,7 @@ function executeCalls(value: unknown): ExecuteCall[] {
|
||||
// The `execute` tool streams child tool calls through metadata, not a child session like Task.
|
||||
function Execute(props: ToolProps) {
|
||||
const ctx = use()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const isLoading = createMemo(() => props.part.state.status === "streaming" || props.part.state.status === "running")
|
||||
const calls = createMemo(() => executeCalls(props.metadata.toolCalls))
|
||||
const output = createMemo(() => stripAnsi(props.output?.trim() ?? ""))
|
||||
@@ -2599,7 +2619,7 @@ function Execute(props: ToolProps) {
|
||||
<>
|
||||
<InlineTool
|
||||
icon={hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│"}
|
||||
color={hasRuntimeError() ? theme.error : undefined}
|
||||
color={hasRuntimeError() ? themeV2.text.feedback.error() : undefined}
|
||||
spinner={isLoading()}
|
||||
pending="execute"
|
||||
complete={true}
|
||||
@@ -2611,7 +2631,7 @@ function Execute(props: ToolProps) {
|
||||
<box paddingLeft={3}>
|
||||
<For each={outputPreview().split("\n")}>
|
||||
{(line, index) => (
|
||||
<text paddingLeft={3} fg={theme.error}>
|
||||
<text paddingLeft={3} fg={themeV2.text.feedback.error()}>
|
||||
{index() === 0 ? "↳ " : " "}
|
||||
{line}
|
||||
</text>
|
||||
@@ -2625,7 +2645,7 @@ function Execute(props: ToolProps) {
|
||||
|
||||
function Edit(props: ToolProps) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const pathFormatter = usePathFormatter()
|
||||
|
||||
const view = createMemo(() => {
|
||||
@@ -2653,16 +2673,16 @@ function Edit(props: ToolProps) {
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode={ctx.diffWrapMode()}
|
||||
fg={theme.text}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
fg={themeV2.text()}
|
||||
addedBg={themeV2.diff.background.added()}
|
||||
removedBg={themeV2.diff.background.removed()}
|
||||
contextBg={themeV2.diff.background.context()}
|
||||
addedSignColor={themeV2.diff.highlight.added()}
|
||||
removedSignColor={themeV2.diff.highlight.removed()}
|
||||
lineNumberFg={themeV2.diff.lineNumber.text()}
|
||||
lineNumberBg={themeV2.diff.background.context()}
|
||||
addedLineNumberBg={themeV2.diff.lineNumber.background.added()}
|
||||
removedLineNumberBg={themeV2.diff.lineNumber.background.removed()}
|
||||
/>
|
||||
</box>
|
||||
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={stringValue(props.input.path) ?? ""} />
|
||||
@@ -2687,7 +2707,7 @@ function Edit(props: ToolProps) {
|
||||
|
||||
function ApplyPatch(props: ToolProps) {
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
const { themeV2, syntax } = useTheme()
|
||||
const pathFormatter = usePathFormatter()
|
||||
const files = createMemo(() => parseApplyPatchFiles(props.metadata.files))
|
||||
const targets = createMemo(() => {
|
||||
@@ -2727,7 +2747,7 @@ function ApplyPatch(props: ToolProps) {
|
||||
<Show
|
||||
when={file.type !== "delete"}
|
||||
fallback={
|
||||
<text fg={theme.diffRemoved}>
|
||||
<text fg={themeV2.diff.text.removed()}>
|
||||
-{file.deletions} line{file.deletions !== 1 ? "s" : ""}
|
||||
</text>
|
||||
}
|
||||
@@ -2741,16 +2761,16 @@ function ApplyPatch(props: ToolProps) {
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode={ctx.diffWrapMode()}
|
||||
fg={theme.text}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
fg={themeV2.text()}
|
||||
addedBg={themeV2.diff.background.added()}
|
||||
removedBg={themeV2.diff.background.removed()}
|
||||
contextBg={themeV2.diff.background.context()}
|
||||
addedSignColor={themeV2.diff.highlight.added()}
|
||||
removedSignColor={themeV2.diff.highlight.removed()}
|
||||
lineNumberFg={themeV2.diff.lineNumber.text()}
|
||||
lineNumberBg={themeV2.diff.background.context()}
|
||||
addedLineNumberBg={themeV2.diff.lineNumber.background.added()}
|
||||
removedLineNumberBg={themeV2.diff.lineNumber.background.removed()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -2773,7 +2793,7 @@ function ApplyPatch(props: ToolProps) {
|
||||
<FilePath
|
||||
value={file.resource}
|
||||
maxWidth={Math.max(2, ctx.width - 3)}
|
||||
fg={file.type === "delete" ? theme.diffRemoved : theme.textMuted}
|
||||
fg={file.type === "delete" ? themeV2.diff.text.removed() : themeV2.text.subdued()}
|
||||
/>
|
||||
</BlockTool>
|
||||
)}
|
||||
@@ -2802,7 +2822,7 @@ function ApplyPatch(props: ToolProps) {
|
||||
}
|
||||
|
||||
function Question(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const questions = createMemo(() => parseQuestions(props.input.questions))
|
||||
const answers = createMemo(() => parseQuestionAnswers(props.metadata.answers))
|
||||
const count = createMemo(() => questions().length)
|
||||
@@ -2820,8 +2840,8 @@ function Question(props: ToolProps) {
|
||||
<For each={questions()}>
|
||||
{(q, i) => (
|
||||
<box flexDirection="column">
|
||||
<text fg={theme.textMuted}>{q.question}</text>
|
||||
<text fg={theme.text}>{format(answers()?.[i()])}</text>
|
||||
<text fg={themeV2.text.subdued()}>{q.question}</text>
|
||||
<text fg={themeV2.text()}>{format(answers()?.[i()])}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
@@ -2847,7 +2867,7 @@ function Skill(props: ToolProps) {
|
||||
}
|
||||
|
||||
function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
const terminalEnvironment = useTuiTerminalEnvironment()
|
||||
const errors = createMemo(() => {
|
||||
const normalized = normalizePath(
|
||||
@@ -2862,7 +2882,7 @@ function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
|
||||
<box>
|
||||
<For each={errors()}>
|
||||
{(diagnostic) => (
|
||||
<text fg={theme.error}>
|
||||
<text fg={themeV2.text.feedback.error()}>
|
||||
Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] {diagnostic.message}
|
||||
</text>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { dirname } from "node:path"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useTheme, selectedForeground } from "../../context/theme"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import type { PermissionV2Request } from "@opencode-ai/client"
|
||||
import { useClient } from "../../context/client"
|
||||
import { SplitBorder } from "../../ui/border"
|
||||
@@ -20,7 +20,7 @@ type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
const themeState = useTheme()
|
||||
const theme = themeState.theme
|
||||
const themeV2 = themeState.themeV2
|
||||
const syntax = themeState.syntax
|
||||
const config = useConfig().data
|
||||
const dimensions = useTerminalDimensions()
|
||||
@@ -51,8 +51,8 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
scrollAcceleration={scrollAcceleration()}
|
||||
verticalScrollbarOptions={{
|
||||
trackOptions: {
|
||||
backgroundColor: theme.background,
|
||||
foregroundColor: theme.borderActive,
|
||||
backgroundColor: themeV2.background(),
|
||||
foregroundColor: themeV2.scrollbar(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -64,16 +64,16 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode="word"
|
||||
fg={theme.text}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
fg={themeV2.text()}
|
||||
addedBg={themeV2.diff.background.added()}
|
||||
removedBg={themeV2.diff.background.removed()}
|
||||
contextBg={themeV2.diff.background.context()}
|
||||
addedSignColor={themeV2.diff.highlight.added()}
|
||||
removedSignColor={themeV2.diff.highlight.removed()}
|
||||
lineNumberFg={themeV2.diff.lineNumber.text()}
|
||||
lineNumberBg={themeV2.diff.background.context()}
|
||||
addedLineNumberBg={themeV2.diff.lineNumber.background.added()}
|
||||
removedLineNumberBg={themeV2.diff.lineNumber.background.removed()}
|
||||
/>
|
||||
</scrollbox>
|
||||
</Show>
|
||||
@@ -82,7 +82,7 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
when={props.patch}
|
||||
fallback={
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>No diff provided</text>
|
||||
<text fg={themeV2.text.subdued()}>No diff provided</text>
|
||||
</box>
|
||||
}
|
||||
>
|
||||
@@ -92,8 +92,8 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
scrollAcceleration={scrollAcceleration()}
|
||||
verticalScrollbarOptions={{
|
||||
trackOptions: {
|
||||
backgroundColor: theme.background,
|
||||
foregroundColor: theme.borderActive,
|
||||
backgroundColor: themeV2.background(),
|
||||
foregroundColor: themeV2.scrollbar(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -103,7 +103,7 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
streaming={true}
|
||||
syntaxStyle={syntax()}
|
||||
content={patch()}
|
||||
fg={theme.textMuted}
|
||||
fg={themeV2.text.subdued()}
|
||||
/>
|
||||
</scrollbox>
|
||||
)}
|
||||
@@ -114,20 +114,20 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
|
||||
}
|
||||
|
||||
function TextBody(props: { title: string; description?: string; icon?: string }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
return (
|
||||
<>
|
||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||
<Show when={props.icon}>
|
||||
<text fg={theme.textMuted} flexShrink={0}>
|
||||
<text fg={themeV2.text.subdued()} flexShrink={0}>
|
||||
{props.icon}
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>{props.title}</text>
|
||||
<text fg={themeV2.text.subdued()}>{props.title}</text>
|
||||
</box>
|
||||
<Show when={props.description}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>{props.description}</text>
|
||||
<text fg={themeV2.text()}>{props.description}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</>
|
||||
@@ -153,7 +153,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
return {}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme()
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@@ -167,11 +167,11 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<text fg={theme.textMuted}>This will allow the following patterns until OpenCode is restarted</text>
|
||||
<text fg={themeV2.text.subdued()}>This will allow the following patterns until OpenCode is restarted</text>
|
||||
<box>
|
||||
<For each={props.request.save ?? []}>
|
||||
{(pattern) => (
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
{"- "}
|
||||
{pattern}
|
||||
</text>
|
||||
@@ -235,7 +235,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={filePath}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Path: " + pathFormatter.format(filePath)}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Path: " + pathFormatter.format(filePath)}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -250,7 +250,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={pattern}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Pattern: " + pattern}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -265,7 +265,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={pattern}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Pattern: " + pattern}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -281,7 +281,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={dir}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Path: " + pathFormatter.format(dir)}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Path: " + pathFormatter.format(dir)}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -294,7 +294,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={command}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>{"$ " + command}</text>
|
||||
<text fg={themeV2.text()}>{"$ " + command}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -315,7 +315,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={desc}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.text}>{"◉ " + desc}</text>
|
||||
<text fg={themeV2.text()}>{"◉ " + desc}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -330,7 +330,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={url}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"URL: " + url}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"URL: " + url}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -345,7 +345,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={query}>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Query: " + query}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Query: " + query}</text>
|
||||
</box>
|
||||
</Show>
|
||||
),
|
||||
@@ -370,9 +370,9 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
body: (
|
||||
<Show when={patterns.length > 0}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<text fg={theme.textMuted}>Patterns</text>
|
||||
<text fg={themeV2.text.subdued()}>Patterns</text>
|
||||
<box>
|
||||
<For each={patterns}>{(p) => <text fg={theme.text}>{"- " + p}</text>}</For>
|
||||
<For each={patterns}>{(p) => <text fg={themeV2.text()}>{"- " + p}</text>}</For>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
@@ -386,7 +386,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
title: "Continue after repeated failures",
|
||||
body: (
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>This keeps the session running despite repeated failures.</text>
|
||||
<text fg={themeV2.text.subdued()}>This keeps the session running despite repeated failures.</text>
|
||||
</box>
|
||||
),
|
||||
}
|
||||
@@ -397,7 +397,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
title: `Call tool ${permission}`,
|
||||
body: (
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{"Tool: " + permission}</text>
|
||||
<text fg={themeV2.text.subdued()}>{"Tool: " + permission}</text>
|
||||
</box>
|
||||
),
|
||||
}
|
||||
@@ -408,15 +408,15 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
const header = () => (
|
||||
<box flexDirection="column" gap={0}>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<text fg={theme.warning}>{"△"}</text>
|
||||
<text fg={theme.text}>Permission required</text>
|
||||
<text fg={themeV2.text.feedback.warning()}>{"△"}</text>
|
||||
<text fg={themeV2.text()}>Permission required</text>
|
||||
</box>
|
||||
<Show when={current.title}>
|
||||
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
|
||||
<text fg={theme.textMuted} flexShrink={0}>
|
||||
<text fg={themeV2.text.subdued()} flexShrink={0}>
|
||||
{current.icon}
|
||||
</text>
|
||||
<text fg={theme.text}>{current.title}</text>
|
||||
<text fg={themeV2.text()}>{current.title}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
@@ -469,7 +469,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
|
||||
function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: () => void }) {
|
||||
let input: TextareaRenderable
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const narrow = createMemo(() => dimensions().width < 80)
|
||||
Keymap.createLayer(() => ({
|
||||
@@ -495,18 +495,18 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||
|
||||
return (
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
border={["left"]}
|
||||
borderColor={theme.error}
|
||||
borderColor={themeV2.text.feedback.error()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||
<text fg={theme.error}>{"△"}</text>
|
||||
<text fg={theme.text}>Reject permission</text>
|
||||
<text fg={themeV2.text.feedback.error()}>{"△"}</text>
|
||||
<text fg={themeV2.text()}>Reject permission</text>
|
||||
</box>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>Tell OpenCode what to do differently</text>
|
||||
<text fg={themeV2.text.subdued()}>Tell OpenCode what to do differently</text>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
@@ -516,7 +516,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||
paddingLeft={2}
|
||||
paddingRight={3}
|
||||
paddingBottom={1}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
backgroundColor={themeV2.background.action.secondary("focused")}
|
||||
justifyContent={narrow() ? "flex-start" : "space-between"}
|
||||
alignItems={narrow() ? "flex-start" : "center"}
|
||||
gap={1}
|
||||
@@ -527,16 +527,16 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
||||
val.traits = { status: "REJECT" }
|
||||
}}
|
||||
focused
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
textColor={themeV2.text()}
|
||||
focusedTextColor={themeV2.text()}
|
||||
cursorColor={themeV2.text()}
|
||||
/>
|
||||
<box flexDirection="row" gap={2} flexShrink={0}>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
||||
<text fg={themeV2.text()}>
|
||||
enter <span style={{ fg: themeV2.text.subdued() }}>confirm</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
||||
<text fg={themeV2.text()}>
|
||||
esc <span style={{ fg: themeV2.text.subdued() }}>cancel</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
@@ -553,7 +553,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
fullscreen?: boolean
|
||||
onSelect: (option: keyof T) => void
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const keys = Object.keys(props.options) as (keyof T)[]
|
||||
const [store, setStore] = createStore({
|
||||
@@ -654,9 +654,9 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
|
||||
const content = () => (
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
border={["left"]}
|
||||
borderColor={theme.warning}
|
||||
borderColor={themeV2.text.feedback.warning()}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
{...(store.expanded
|
||||
? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
|
||||
@@ -674,8 +674,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
when={props.header}
|
||||
fallback={
|
||||
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
||||
<text fg={theme.warning}>{"△"}</text>
|
||||
<text fg={theme.text}>{props.title}</text>
|
||||
<text fg={themeV2.text.feedback.warning()}>{"△"}</text>
|
||||
<text fg={themeV2.text()}>{props.title}</text>
|
||||
</box>
|
||||
}
|
||||
>
|
||||
@@ -693,7 +693,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
paddingLeft={2}
|
||||
paddingRight={3}
|
||||
paddingBottom={1}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
backgroundColor={themeV2.background.action.secondary("focused")}
|
||||
justifyContent={narrow() ? "flex-start" : "space-between"}
|
||||
alignItems={narrow() ? "flex-start" : "center"}
|
||||
>
|
||||
@@ -703,14 +703,20 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu}
|
||||
backgroundColor={themeV2.background.action.primary(
|
||||
option === store.selected ? "focused" : "default",
|
||||
)}
|
||||
onMouseOver={() => setStore("selected", option)}
|
||||
onMouseUp={() => {
|
||||
setStore("selected", option)
|
||||
props.onSelect(option)
|
||||
}}
|
||||
>
|
||||
<text fg={option === store.selected ? selectedForeground(theme, theme.warning) : theme.textMuted}>
|
||||
<text
|
||||
fg={themeV2.text.action.primary(
|
||||
option === store.selected ? "focused" : "default",
|
||||
)}
|
||||
>
|
||||
{props.options[option]}
|
||||
</text>
|
||||
</box>
|
||||
@@ -719,15 +725,15 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
</box>
|
||||
<box flexDirection="row" gap={2} flexShrink={0}>
|
||||
<Show when={props.fullscreen}>
|
||||
<text fg={theme.text}>
|
||||
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: theme.textMuted }}>{hint()}</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{shortcuts.get("permission.prompt.fullscreen")} <span style={{ fg: themeV2.text.subdued() }}>{hint()}</span>
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
|
||||
<text fg={themeV2.text()}>
|
||||
{"⇆"} <span style={{ fg: themeV2.text.subdued() }}>select</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
||||
<text fg={themeV2.text()}>
|
||||
enter <span style={{ fg: themeV2.text.subdued() }}>confirm</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getScrollAcceleration } from "../../util/scroll"
|
||||
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
const pluginRuntime = usePluginRuntime()
|
||||
const data = useData()
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const config = useConfig().data
|
||||
const session = createMemo(() => data.session.get(props.sessionID))
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||
@@ -18,7 +18,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
return (
|
||||
<Show when={session()}>
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
width={42}
|
||||
height="100%"
|
||||
paddingTop={1}
|
||||
@@ -32,8 +32,8 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
scrollAcceleration={scrollAcceleration()}
|
||||
verticalScrollbarOptions={{
|
||||
trackOptions: {
|
||||
backgroundColor: theme.background,
|
||||
foregroundColor: theme.borderActive,
|
||||
backgroundColor: themeV2.background(),
|
||||
foregroundColor: themeV2.scrollbar(),
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -45,11 +45,11 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
title={session()!.title}
|
||||
>
|
||||
<box paddingRight={1}>
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
<b>{session()!.title}</b>
|
||||
</text>
|
||||
<Show when={session()!.location.workspaceID}>
|
||||
<text fg={theme.textMuted}>{session()!.location.workspaceID}</text>
|
||||
<text fg={themeV2.text.subdued()}>{session()!.location.workspaceID}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</pluginRuntime.Slot>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SubagentFooter() {
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const keymap = Keymap.use()
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
|
||||
@@ -61,18 +61,18 @@ export function SubagentFooter() {
|
||||
paddingRight={1}
|
||||
{...SplitBorder}
|
||||
border={["left"]}
|
||||
borderColor={theme.border}
|
||||
borderColor={themeV2.border()}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
backgroundColor={themeV2.background()}
|
||||
>
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.text}>
|
||||
<text fg={themeV2.text()}>
|
||||
<b>{subagentInfo()}</b>
|
||||
</text>
|
||||
<Show when={usage()}>
|
||||
{(item) => (
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
<text fg={themeV2.text.subdued()} wrapMode="none">
|
||||
{[item().context, item().cost].filter(Boolean).join(" · ")}
|
||||
</text>
|
||||
)}
|
||||
@@ -83,30 +83,30 @@ export function SubagentFooter() {
|
||||
onMouseOver={() => setHover("parent")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => keymap.dispatch("session.parent")}
|
||||
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() === "parent" ? themeV2.background.action.secondary("focused") : themeV2.background()}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Parent <span style={{ fg: theme.textMuted }}>{shortcuts.get("session.parent")}</span>
|
||||
<text fg={themeV2.text()}>
|
||||
Parent <span style={{ fg: themeV2.text.subdued() }}>{shortcuts.get("session.parent")}</span>
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
onMouseOver={() => setHover("prev")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => keymap.dispatch("session.child.previous")}
|
||||
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() === "prev" ? themeV2.background.action.secondary("focused") : themeV2.background()}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Prev <span style={{ fg: theme.textMuted }}>{shortcuts.get("session.child.previous")}</span>
|
||||
<text fg={themeV2.text()}>
|
||||
Prev <span style={{ fg: themeV2.text.subdued() }}>{shortcuts.get("session.child.previous")}</span>
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
onMouseOver={() => setHover("next")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => keymap.dispatch("session.child.next")}
|
||||
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
backgroundColor={hover() === "next" ? themeV2.background.action.secondary("focused") : themeV2.background()}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Next <span style={{ fg: theme.textMuted }}>{shortcuts.get("session.child.next")}</span>
|
||||
<text fg={themeV2.text()}>
|
||||
Next <span style={{ fg: themeV2.text.subdued() }}>{shortcuts.get("session.child.next")}</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { ActionState, ActionVariant, ResolvedActionState, ResolvedThemeView } from "./index"
|
||||
import type {
|
||||
ActionState,
|
||||
ActionVariant,
|
||||
FormfieldState,
|
||||
ResolvedActionState,
|
||||
ResolvedFormfieldState,
|
||||
ResolvedThemeView,
|
||||
} from "./index"
|
||||
|
||||
export function createComponentTheme(current: Accessor<ResolvedThemeView>) {
|
||||
const textAction = actions((variant, state) => current().color.text.action[variant][state])
|
||||
const backgroundAction = actions((variant, state) => current().color.background.action[variant][state])
|
||||
const text = Object.assign(() => current().color.text.default, {
|
||||
subdued: () => current().color.text.subdued,
|
||||
const textAction = actions((variant, state) => current().text.action[variant][state])
|
||||
const backgroundAction = actions((variant, state) => current().background.action[variant][state])
|
||||
const textFormfield = formfield((state) => current().text.formfield[state])
|
||||
const backgroundFormfield = formfield((state) => current().background.formfield[state])
|
||||
const text = Object.assign(() => current().text.default, {
|
||||
subdued: () => current().text.subdued,
|
||||
action: textAction,
|
||||
formfield: textFormfield,
|
||||
feedback: {
|
||||
error: feedbackText("error"),
|
||||
warning: feedbackText("warning"),
|
||||
@@ -15,81 +25,84 @@ export function createComponentTheme(current: Accessor<ResolvedThemeView>) {
|
||||
info: feedbackText("info"),
|
||||
},
|
||||
})
|
||||
const background = Object.assign(() => current().color.background.default, {
|
||||
const background = Object.assign(() => current().background.default, {
|
||||
surface: {
|
||||
offset: () => current().background.surface.offset,
|
||||
overlay: () => current().background.surface.overlay,
|
||||
},
|
||||
action: backgroundAction,
|
||||
formfield: backgroundFormfield,
|
||||
feedback: {
|
||||
error: () => current().color.background.feedback.error.default,
|
||||
warning: () => current().color.background.feedback.warning.default,
|
||||
success: () => current().color.background.feedback.success.default,
|
||||
info: () => current().color.background.feedback.info.default,
|
||||
error: () => current().background.feedback.error.default,
|
||||
warning: () => current().background.feedback.warning.default,
|
||||
success: () => current().background.feedback.success.default,
|
||||
info: () => current().background.feedback.info.default,
|
||||
},
|
||||
})
|
||||
const markdown = Object.assign(() => current().color.markdown.text, {
|
||||
heading: () => current().color.markdown.heading,
|
||||
link: () => current().color.markdown.link,
|
||||
linkText: () => current().color.markdown.linkText,
|
||||
code: () => current().color.markdown.code,
|
||||
blockQuote: () => current().color.markdown.blockQuote,
|
||||
emphasis: () => current().color.markdown.emphasis,
|
||||
strong: () => current().color.markdown.strong,
|
||||
horizontalRule: () => current().color.markdown.horizontalRule,
|
||||
listItem: () => current().color.markdown.listItem,
|
||||
listEnumeration: () => current().color.markdown.listEnumeration,
|
||||
image: () => current().color.markdown.image,
|
||||
imageText: () => current().color.markdown.imageText,
|
||||
codeBlock: () => current().color.markdown.codeBlock,
|
||||
const markdown = Object.assign(() => current().markdown.text, {
|
||||
heading: () => current().markdown.heading,
|
||||
link: () => current().markdown.link,
|
||||
linkText: () => current().markdown.linkText,
|
||||
code: () => current().markdown.code,
|
||||
blockQuote: () => current().markdown.blockQuote,
|
||||
emphasis: () => current().markdown.emphasis,
|
||||
strong: () => current().markdown.strong,
|
||||
horizontalRule: () => current().markdown.horizontalRule,
|
||||
listItem: () => current().markdown.listItem,
|
||||
listEnumeration: () => current().markdown.listEnumeration,
|
||||
image: () => current().markdown.image,
|
||||
imageText: () => current().markdown.imageText,
|
||||
codeBlock: () => current().markdown.codeBlock,
|
||||
})
|
||||
|
||||
function feedbackText(kind: "error" | "warning" | "success" | "info") {
|
||||
return Object.assign(() => current().color.text.feedback[kind].default, {
|
||||
subdued: () => current().color.text.feedback[kind].subdued,
|
||||
return Object.assign(() => current().text.feedback[kind].default, {
|
||||
subdued: () => current().text.feedback[kind].subdued,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
hue: () => current().hue,
|
||||
color: {
|
||||
text,
|
||||
background,
|
||||
border: () => current().color.border.default,
|
||||
scrollbar: () => current().color.scrollbar.default,
|
||||
diff: {
|
||||
text,
|
||||
background,
|
||||
border: () => current().border.default,
|
||||
scrollbar: () => current().scrollbar.default,
|
||||
diff: {
|
||||
text: {
|
||||
added: () => current().color.diff.text.added,
|
||||
removed: () => current().color.diff.text.removed,
|
||||
context: () => current().color.diff.text.context,
|
||||
hunkHeader: () => current().color.diff.text.hunkHeader,
|
||||
added: () => current().diff.text.added,
|
||||
removed: () => current().diff.text.removed,
|
||||
context: () => current().diff.text.context,
|
||||
hunkHeader: () => current().diff.text.hunkHeader,
|
||||
},
|
||||
background: {
|
||||
added: () => current().color.diff.background.added,
|
||||
removed: () => current().color.diff.background.removed,
|
||||
context: () => current().color.diff.background.context,
|
||||
added: () => current().diff.background.added,
|
||||
removed: () => current().diff.background.removed,
|
||||
context: () => current().diff.background.context,
|
||||
},
|
||||
highlight: {
|
||||
added: () => current().color.diff.highlight.added,
|
||||
removed: () => current().color.diff.highlight.removed,
|
||||
added: () => current().diff.highlight.added,
|
||||
removed: () => current().diff.highlight.removed,
|
||||
},
|
||||
lineNumber: {
|
||||
text: () => current().color.diff.lineNumber.text,
|
||||
text: () => current().diff.lineNumber.text,
|
||||
background: {
|
||||
added: () => current().color.diff.lineNumber.background.added,
|
||||
removed: () => current().color.diff.lineNumber.background.removed,
|
||||
added: () => current().diff.lineNumber.background.added,
|
||||
removed: () => current().diff.lineNumber.background.removed,
|
||||
},
|
||||
},
|
||||
},
|
||||
syntax: {
|
||||
comment: () => current().color.syntax.comment,
|
||||
keyword: () => current().color.syntax.keyword,
|
||||
function: () => current().color.syntax.function,
|
||||
variable: () => current().color.syntax.variable,
|
||||
string: () => current().color.syntax.string,
|
||||
number: () => current().color.syntax.number,
|
||||
type: () => current().color.syntax.type,
|
||||
operator: () => current().color.syntax.operator,
|
||||
punctuation: () => current().color.syntax.punctuation,
|
||||
},
|
||||
markdown,
|
||||
syntax: {
|
||||
comment: () => current().syntax.comment,
|
||||
keyword: () => current().syntax.keyword,
|
||||
function: () => current().syntax.function,
|
||||
variable: () => current().syntax.variable,
|
||||
string: () => current().syntax.string,
|
||||
number: () => current().syntax.number,
|
||||
type: () => current().syntax.type,
|
||||
operator: () => current().syntax.operator,
|
||||
punctuation: () => current().syntax.punctuation,
|
||||
},
|
||||
markdown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +116,8 @@ function actions(get: (variant: ActionVariant, state: ResolvedActionState) => RG
|
||||
})
|
||||
}
|
||||
|
||||
function formfield(get: (state: ResolvedFormfieldState) => RGBA) {
|
||||
return (state: FormfieldState | "default" = "default") => get(state)
|
||||
}
|
||||
|
||||
export type ComponentTheme = ReturnType<typeof createComponentTheme>
|
||||
|
||||
@@ -95,8 +95,7 @@ export const DEFAULT_THEME = {
|
||||
accent: "$hue.blue",
|
||||
neutral: "$hue.gray",
|
||||
},
|
||||
color: {
|
||||
text: {
|
||||
text: {
|
||||
default: "$hue.neutral.900",
|
||||
subdued: "$hue.neutral.600",
|
||||
action: {
|
||||
@@ -104,6 +103,13 @@ export const DEFAULT_THEME = {
|
||||
secondary: { default: "$hue.neutral.900", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
|
||||
},
|
||||
formfield: {
|
||||
default: "$hue.neutral.900",
|
||||
$focused: "$text.action.primary.default",
|
||||
$pressed: "$hue.neutral.100",
|
||||
$disabled: "$hue.neutral.500",
|
||||
$selected: "$hue.accent.600",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$hue.red.700", subdued: "$hue.red.600" },
|
||||
warning: { default: "$hue.yellow.800", subdued: "$hue.yellow.700" },
|
||||
@@ -113,25 +119,36 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.100",
|
||||
surface: {
|
||||
offset: "$hue.neutral.200",
|
||||
overlay: "$hue.neutral.300",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
default: "$hue.accent.600", $hovered: "$hue.accent.700", $pressed: "$hue.accent.800",
|
||||
$selected: "$hue.accent.700", $disabled: "$hue.neutral.300",
|
||||
default: "$hue.accent.600", $focused: "$hue.accent.700", $pressed: "$hue.accent.800",
|
||||
$disabled: "$hue.neutral.300",
|
||||
},
|
||||
secondary: {
|
||||
default: "$hue.neutral.200", $hovered: "$hue.neutral.300", $pressed: "$hue.neutral.400",
|
||||
$selected: "$hue.neutral.300", $disabled: "$hue.neutral.200",
|
||||
default: "$hue.neutral.200", $focused: "$hue.neutral.300", $pressed: "$hue.neutral.400",
|
||||
$disabled: "$hue.neutral.200",
|
||||
},
|
||||
destructive: {
|
||||
default: "$hue.red.600", $hovered: "$hue.red.700", $pressed: "$hue.red.800",
|
||||
$selected: "$hue.red.700", $disabled: "$hue.neutral.300",
|
||||
default: "$hue.red.600", $focused: "$hue.red.700", $pressed: "$hue.red.800",
|
||||
$disabled: "$hue.neutral.300",
|
||||
},
|
||||
},
|
||||
formfield: {
|
||||
default: "$background.default",
|
||||
$focused: "$background.action.primary.default",
|
||||
$pressed: "$hue.accent.800",
|
||||
$disabled: "$background.default",
|
||||
$selected: "$background.formfield.default",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$color.background.default" },
|
||||
warning: { default: "$color.background.default" },
|
||||
success: { default: "$color.background.default" },
|
||||
info: { default: "$color.background.default" },
|
||||
error: { default: "$background.default" },
|
||||
warning: { default: "$background.default" },
|
||||
success: { default: "$background.default" },
|
||||
info: { default: "$background.default" },
|
||||
},
|
||||
},
|
||||
border: { default: "$hue.neutral.300" },
|
||||
@@ -174,24 +191,19 @@ export const DEFAULT_THEME = {
|
||||
image: "$hue.accent.600",
|
||||
imageText: "$hue.cyan.600",
|
||||
codeBlock: "$hue.neutral.900",
|
||||
},
|
||||
},
|
||||
"@context:elevated": {
|
||||
color: {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$hue.neutral.200",
|
||||
action: { primary: { default: "$hue.accent.500" } },
|
||||
},
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { default: "$hue.accent.500" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": {
|
||||
color: {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$hue.neutral.300",
|
||||
action: { primary: { default: "$hue.accent.500" } },
|
||||
},
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$background.surface.overlay",
|
||||
action: { primary: { default: "$hue.accent.500" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -288,8 +300,7 @@ export const DEFAULT_THEME = {
|
||||
accent: "$hue.blue",
|
||||
neutral: "$hue.gray",
|
||||
},
|
||||
color: {
|
||||
text: {
|
||||
text: {
|
||||
default: "$hue.neutral.100",
|
||||
subdued: "$hue.neutral.400",
|
||||
action: {
|
||||
@@ -297,6 +308,13 @@ export const DEFAULT_THEME = {
|
||||
secondary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
|
||||
},
|
||||
formfield: {
|
||||
default: "$hue.neutral.100",
|
||||
$focused: "$text.action.primary.default",
|
||||
$pressed: "$hue.neutral.100",
|
||||
$disabled: "$hue.neutral.500",
|
||||
$selected: "$hue.accent.500",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
|
||||
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
|
||||
@@ -306,25 +324,36 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.900",
|
||||
surface: {
|
||||
offset: "$hue.neutral.800",
|
||||
overlay: "$hue.neutral.700",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
default: "$hue.accent.500", $hovered: "$hue.accent.600", $pressed: "$hue.accent.800",
|
||||
$selected: "$hue.accent.600", $disabled: "$hue.neutral.800",
|
||||
default: "$hue.accent.500", $focused: "$hue.accent.600", $pressed: "$hue.accent.800",
|
||||
$disabled: "$hue.neutral.800",
|
||||
},
|
||||
secondary: {
|
||||
default: "$hue.neutral.800", $hovered: "$hue.neutral.700", $pressed: "$hue.neutral.900",
|
||||
$selected: "$hue.neutral.700", $disabled: "$hue.neutral.900",
|
||||
default: "$hue.neutral.800", $focused: "$hue.neutral.700", $pressed: "$hue.neutral.900",
|
||||
$disabled: "$hue.neutral.900",
|
||||
},
|
||||
destructive: {
|
||||
default: "$hue.red.600", $hovered: "$hue.red.700", $pressed: "$hue.red.800",
|
||||
$selected: "$hue.red.700", $disabled: "$hue.neutral.800",
|
||||
default: "$hue.red.600", $focused: "$hue.red.700", $pressed: "$hue.red.800",
|
||||
$disabled: "$hue.neutral.800",
|
||||
},
|
||||
},
|
||||
formfield: {
|
||||
default: "$background.default",
|
||||
$focused: "$background.action.primary.default",
|
||||
$pressed: "$hue.accent.800",
|
||||
$disabled: "$background.default",
|
||||
$selected: "$background.formfield.default",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$color.background.default" },
|
||||
warning: { default: "$color.background.default" },
|
||||
success: { default: "$color.background.default" },
|
||||
info: { default: "$color.background.default" },
|
||||
error: { default: "$background.default" },
|
||||
warning: { default: "$background.default" },
|
||||
success: { default: "$background.default" },
|
||||
info: { default: "$background.default" },
|
||||
},
|
||||
},
|
||||
border: { default: "$hue.neutral.700" },
|
||||
@@ -367,24 +396,19 @@ export const DEFAULT_THEME = {
|
||||
image: "$hue.accent.400",
|
||||
imageText: "$hue.cyan.400",
|
||||
codeBlock: "$hue.neutral.100",
|
||||
},
|
||||
},
|
||||
"@context:elevated": {
|
||||
color: {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$hue.neutral.800",
|
||||
action: { primary: { default: "$hue.accent.400" } },
|
||||
},
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { default: "$hue.accent.400" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": {
|
||||
color: {
|
||||
text: { action: { primary: { default: "$hue.neutral.900" } } },
|
||||
background: {
|
||||
default: "$hue.neutral.700",
|
||||
action: { primary: { default: "$hue.accent.400" } },
|
||||
},
|
||||
text: { action: { primary: { default: "$hue.neutral.900" } } },
|
||||
background: {
|
||||
default: "$background.surface.overlay",
|
||||
action: { primary: { default: "$hue.accent.400" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type {
|
||||
BackgroundDefinition,
|
||||
FormfieldColorDefinition,
|
||||
ModeDefinition,
|
||||
StatefulColorDefinition,
|
||||
TextDefinition,
|
||||
ThemeTokensDefinition,
|
||||
} from "./index"
|
||||
import { ActionState } from "./schema"
|
||||
import { ActionState, FormfieldState } from "./schema"
|
||||
|
||||
export function expandTheme<Definition extends ModeDefinition>(definition: Definition): Definition {
|
||||
return {
|
||||
@@ -20,14 +21,10 @@ export function expandTheme<Definition extends ModeDefinition>(definition: Defin
|
||||
}
|
||||
|
||||
export function expandTokens(definition: ThemeTokensDefinition): ThemeTokensDefinition {
|
||||
if (!definition.color) return { ...definition }
|
||||
return {
|
||||
...definition,
|
||||
color: {
|
||||
...definition.color,
|
||||
text: expandText(definition.color.text),
|
||||
background: expandBackground(definition.color.background),
|
||||
},
|
||||
text: expandText(definition.text),
|
||||
background: expandBackground(definition.background),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +45,9 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
|
||||
if (!definition) return
|
||||
return {
|
||||
...definition,
|
||||
subdued: definition.subdued ?? (definition.default ? "$color.text.default" : undefined),
|
||||
action: expandActions(definition.action, "color.text.action"),
|
||||
subdued: definition.subdued ?? (definition.default ? "$text.default" : undefined),
|
||||
action: expandActions(definition.action, "text.action"),
|
||||
formfield: expandFormfield(definition.formfield, "text.formfield"),
|
||||
feedback: definition.feedback
|
||||
? Object.fromEntries(
|
||||
Object.entries(definition.feedback).map(([kind, feedback]) => {
|
||||
@@ -57,7 +55,7 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
|
||||
kind,
|
||||
{
|
||||
...feedback,
|
||||
subdued: feedback.subdued ?? (feedback.default ? `$color.text.feedback.${kind}.default` : undefined),
|
||||
subdued: feedback.subdued ?? (feedback.default ? `$text.feedback.${kind}.default` : undefined),
|
||||
},
|
||||
]
|
||||
}),
|
||||
@@ -68,7 +66,21 @@ function expandText(definition: TextDefinition | undefined): TextDefinition | un
|
||||
|
||||
function expandBackground(definition: BackgroundDefinition | undefined): BackgroundDefinition | undefined {
|
||||
if (!definition) return
|
||||
return { ...definition, action: expandActions(definition.action, "color.background.action") }
|
||||
return {
|
||||
...definition,
|
||||
action: expandActions(definition.action, "background.action"),
|
||||
formfield: expandFormfield(definition.formfield, "background.formfield"),
|
||||
}
|
||||
}
|
||||
|
||||
function expandFormfield(definition: FormfieldColorDefinition | undefined, path: string) {
|
||||
if (!definition?.default) return definition
|
||||
return {
|
||||
...definition,
|
||||
...Object.fromEntries(
|
||||
FormfieldState.literals.map((state) => [`$${state}`, definition[`$${state}`] ?? `$${path}.default`]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function expandActions<Definition extends Partial<Record<string, StatefulColorDefinition>>>(
|
||||
|
||||
@@ -5,26 +5,28 @@ export function fallback(): ThemeTokensDefinition {
|
||||
const red = "#ff0000"
|
||||
|
||||
return {
|
||||
color: {
|
||||
text: {
|
||||
text: {
|
||||
default: red,
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
background: {
|
||||
},
|
||||
background: {
|
||||
default: red,
|
||||
surface: { offset: red, overlay: red },
|
||||
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
|
||||
formfield: { default: red },
|
||||
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
|
||||
},
|
||||
border: { default: red },
|
||||
scrollbar: { default: red },
|
||||
diff: {
|
||||
},
|
||||
border: { default: red },
|
||||
scrollbar: { default: red },
|
||||
diff: {
|
||||
text: { added: red, removed: red, context: red, hunkHeader: red },
|
||||
background: { added: red, removed: red, context: red },
|
||||
highlight: { added: red, removed: red },
|
||||
lineNumber: { text: red, background: { added: red, removed: red } },
|
||||
},
|
||||
syntax: {
|
||||
},
|
||||
syntax: {
|
||||
comment: red,
|
||||
keyword: red,
|
||||
function: red,
|
||||
@@ -34,8 +36,8 @@ export function fallback(): ThemeTokensDefinition {
|
||||
type: red,
|
||||
operator: red,
|
||||
punctuation: red,
|
||||
},
|
||||
markdown: {
|
||||
},
|
||||
markdown: {
|
||||
text: red,
|
||||
heading: red,
|
||||
link: red,
|
||||
@@ -50,7 +52,6 @@ export function fallback(): ThemeTokensDefinition {
|
||||
image: red,
|
||||
imageText: red,
|
||||
codeBlock: red,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ export {
|
||||
ActionVariant,
|
||||
BaseHue,
|
||||
FeedbackKind,
|
||||
FormfieldState,
|
||||
type FormfieldStateKey,
|
||||
HueAlias,
|
||||
HueStep,
|
||||
MarkdownDefinition,
|
||||
@@ -16,6 +18,7 @@ export {
|
||||
type BackgroundDefinition,
|
||||
type DiffDefinition,
|
||||
type FileThemeDefinition,
|
||||
type FormfieldColorDefinition,
|
||||
type HueDefinition,
|
||||
type HueOverrideDefinition,
|
||||
type MergeModeDefinition,
|
||||
@@ -26,5 +29,14 @@ export {
|
||||
type ThemeTokensDefinition,
|
||||
} from "./schema"
|
||||
|
||||
export type { Hue, HueScale, ResolvedActionState, ResolvedTheme, ResolvedThemeView, StatefulColor } from "./types"
|
||||
export type {
|
||||
FormfieldColor,
|
||||
Hue,
|
||||
HueScale,
|
||||
ResolvedActionState,
|
||||
ResolvedFormfieldState,
|
||||
ResolvedTheme,
|
||||
ResolvedThemeView,
|
||||
StatefulColor,
|
||||
} from "./types"
|
||||
export { migrateV1 } from "./v1-migrate"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ActionVariant,
|
||||
BaseHue,
|
||||
FeedbackKind,
|
||||
FormfieldState,
|
||||
HueAlias,
|
||||
HueStep,
|
||||
ThemeDefinition,
|
||||
@@ -25,11 +26,33 @@ import type {
|
||||
} from "./index"
|
||||
import { selectTheme, selectThemeMode } from "./select"
|
||||
|
||||
const decodeThemeDefinition = Schema.decodeUnknownSync(ThemeDefinition)
|
||||
const decodeThemeFile = Schema.decodeUnknownSync(ThemeFile)
|
||||
const decodeThemeDefinitionSchema = Schema.decodeUnknownSync(ThemeDefinition)
|
||||
const decodeThemeFileSchema = Schema.decodeUnknownSync(ThemeFile)
|
||||
|
||||
export function resolveThemeFile(file: ThemeFile, mode?: "light" | "dark") {
|
||||
const decoded = decodeThemeFile(file)
|
||||
function decodeThemeDefinition(input: unknown) {
|
||||
try {
|
||||
return decodeThemeDefinitionSchema(input)
|
||||
} catch (error) {
|
||||
throw themeDecodeError(error, "theme")
|
||||
}
|
||||
}
|
||||
|
||||
function decodeThemeFile(input: unknown, name: string) {
|
||||
try {
|
||||
return decodeThemeFileSchema(input)
|
||||
} catch (error) {
|
||||
throw themeDecodeError(error, name)
|
||||
}
|
||||
}
|
||||
|
||||
function themeDecodeError(error: unknown, name: string) {
|
||||
const message = Schema.isSchemaError(error) ? error.message : String(error)
|
||||
const value = /got ("[^"]*"|\S+)/.exec(message)?.[1] ?? "value"
|
||||
return new Error(`Invalid theme: ${name} ${value} is an invalid value`, { cause: error })
|
||||
}
|
||||
|
||||
export function resolveThemeFile(file: ThemeFile, mode?: "light" | "dark", name = "theme") {
|
||||
const decoded = decodeThemeFile(file, name)
|
||||
const selected = selectThemeMode(decoded, mode)
|
||||
const definition = selected.expanded ? selected.theme : expandTheme(selected.theme)
|
||||
const defaults = expandTheme(selectTheme(DEFAULT_THEME, selected.mode))
|
||||
@@ -63,24 +86,28 @@ function resolveExpandedTheme(definition: ThemeDefinition): ResolvedTheme {
|
||||
|
||||
function tokens(definition: ThemeDefinition): ThemeTokensDefinition {
|
||||
return {
|
||||
color: definition.color,
|
||||
text: definition.text,
|
||||
background: definition.background,
|
||||
border: definition.border,
|
||||
scrollbar: definition.scrollbar,
|
||||
diff: definition.diff,
|
||||
syntax: definition.syntax,
|
||||
markdown: definition.markdown,
|
||||
}
|
||||
}
|
||||
|
||||
function contextualize(base: ThemeTokensDefinition, override: ThemeTokensDefinition) {
|
||||
const result = mergeTheme(base, override)
|
||||
const baseText = base.color?.text?.action
|
||||
const contextText = override.color?.text?.action
|
||||
const baseBackground = base.color?.background?.action
|
||||
const contextBackground = override.color?.background?.action
|
||||
const color = result["color"] as NonNullable<ThemeTokensDefinition["color"]>
|
||||
const baseText = base.text?.action
|
||||
const contextText = override.text?.action
|
||||
const baseBackground = base.background?.action
|
||||
const contextBackground = override.background?.action
|
||||
const text = result["text"] as NonNullable<ThemeTokensDefinition["text"]>
|
||||
const background = result["background"] as NonNullable<ThemeTokensDefinition["background"]>
|
||||
return {
|
||||
...result,
|
||||
color: {
|
||||
...color,
|
||||
text: { ...color.text, action: contextualActions(baseText, contextText) },
|
||||
background: { ...color.background, action: contextualActions(baseBackground, contextBackground) },
|
||||
},
|
||||
text: { ...text, action: contextualActions(baseText, contextText) },
|
||||
background: { ...background, action: contextualActions(baseBackground, contextBackground) },
|
||||
} as ThemeTokensDefinition
|
||||
}
|
||||
|
||||
@@ -171,6 +198,7 @@ function createResolver(source: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
function resolveColor(value: string, path: string, stack: string[]) {
|
||||
if (value === "transparent") return RGBA.fromInts(0, 0, 0, 0)
|
||||
if (isHex(value)) return RGBA.fromHex(value)
|
||||
if (!value.startsWith("$")) throw new Error(`Invalid color "${value}" at "${path}"`)
|
||||
const target = value.slice(1)
|
||||
@@ -189,7 +217,7 @@ function createResolver(source: Record<string, unknown>) {
|
||||
function resolvedKey(key: string) {
|
||||
if (!key.startsWith("$")) return key
|
||||
const state = key.slice(1)
|
||||
return (ActionState.literals as readonly string[]).includes(state) ? state : key
|
||||
return ([...ActionState.literals, ...FormfieldState.literals] as readonly string[]).includes(state) ? state : key
|
||||
}
|
||||
|
||||
function read(source: Record<string, unknown>, path: string) {
|
||||
|
||||
@@ -12,10 +12,14 @@ export type HueAlias = Schema.Schema.Type<typeof HueAlias>
|
||||
export const ActionVariant = Schema.Literals(["primary", "secondary", "destructive"])
|
||||
export type ActionVariant = Schema.Schema.Type<typeof ActionVariant>
|
||||
|
||||
export const ActionState = Schema.Literals(["hovered", "pressed", "selected", "focused", "disabled"])
|
||||
export const ActionState = Schema.Literals(["focused", "pressed", "disabled"])
|
||||
export type ActionState = Schema.Schema.Type<typeof ActionState>
|
||||
export type ActionStateKey = `$${ActionState}`
|
||||
|
||||
export const FormfieldState = Schema.Literals(["focused", "pressed", "disabled", "selected"])
|
||||
export type FormfieldState = Schema.Schema.Type<typeof FormfieldState>
|
||||
export type FormfieldStateKey = `$${FormfieldState}`
|
||||
|
||||
export const FeedbackKind = Schema.Literals(["error", "warning", "success", "info"])
|
||||
export type FeedbackKind = Schema.Schema.Type<typeof FeedbackKind>
|
||||
|
||||
@@ -24,7 +28,11 @@ export type Mode = Schema.Schema.Type<typeof Mode>
|
||||
|
||||
const HexColor = Schema.String.check(Schema.isPattern(/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i))
|
||||
|
||||
const ColorValue = Schema.Union([HexColor, Schema.TemplateLiteral(["$", Schema.NonEmptyString])])
|
||||
const ColorValue = Schema.Union([
|
||||
HexColor,
|
||||
Schema.Literal("transparent"),
|
||||
Schema.TemplateLiteral(["$", Schema.NonEmptyString]),
|
||||
])
|
||||
|
||||
const HueName = Schema.Union([BaseHue, HueAlias])
|
||||
const HueColorValue = Schema.Union([HexColor, Schema.TemplateLiteral(["$hue.", HueName, ".", HueStep])])
|
||||
@@ -65,14 +73,21 @@ export type HueOverrideDefinition = Schema.Schema.Type<typeof HueOverrideDefinit
|
||||
|
||||
const StatefulColorDefinition = Schema.Struct({
|
||||
default: Schema.optional(ColorValue),
|
||||
$hovered: Schema.optional(ColorValue),
|
||||
$pressed: Schema.optional(ColorValue),
|
||||
$selected: Schema.optional(ColorValue),
|
||||
$focused: Schema.optional(ColorValue),
|
||||
$pressed: Schema.optional(ColorValue),
|
||||
$disabled: Schema.optional(ColorValue),
|
||||
})
|
||||
export type StatefulColorDefinition = Schema.Schema.Type<typeof StatefulColorDefinition>
|
||||
|
||||
const FormfieldColorDefinition = Schema.Struct({
|
||||
default: Schema.optional(ColorValue),
|
||||
$focused: Schema.optional(ColorValue),
|
||||
$pressed: Schema.optional(ColorValue),
|
||||
$disabled: Schema.optional(ColorValue),
|
||||
$selected: Schema.optional(ColorValue),
|
||||
})
|
||||
export type FormfieldColorDefinition = Schema.Schema.Type<typeof FormfieldColorDefinition>
|
||||
|
||||
const ActionColorDefinition = Schema.Struct({
|
||||
primary: Schema.optional(StatefulColorDefinition),
|
||||
secondary: Schema.optional(StatefulColorDefinition),
|
||||
@@ -92,6 +107,7 @@ const TextDefinition = Schema.Struct({
|
||||
default: Schema.optional(ColorValue),
|
||||
subdued: Schema.optional(ColorValue),
|
||||
action: Schema.optional(ActionColorDefinition),
|
||||
formfield: Schema.optional(FormfieldColorDefinition),
|
||||
feedback: Schema.optional(
|
||||
Schema.Struct({
|
||||
error: Schema.optional(TextFeedbackDefinition),
|
||||
@@ -105,7 +121,14 @@ export type TextDefinition = Schema.Schema.Type<typeof TextDefinition>
|
||||
|
||||
const BackgroundDefinition = Schema.Struct({
|
||||
default: Schema.optional(ColorValue),
|
||||
surface: Schema.optional(
|
||||
Schema.Struct({
|
||||
offset: Schema.optional(ColorValue),
|
||||
overlay: Schema.optional(ColorValue),
|
||||
}),
|
||||
),
|
||||
action: Schema.optional(ActionColorDefinition),
|
||||
formfield: Schema.optional(FormfieldColorDefinition),
|
||||
feedback: Schema.optional(
|
||||
Schema.Struct({
|
||||
error: Schema.optional(BackgroundFeedbackDefinition),
|
||||
@@ -163,17 +186,13 @@ const DiffDefinition = Schema.Struct({
|
||||
export type DiffDefinition = Schema.Schema.Type<typeof DiffDefinition>
|
||||
|
||||
const ThemeTokensDefinition = Schema.Struct({
|
||||
color: Schema.optional(
|
||||
Schema.Struct({
|
||||
text: Schema.optional(TextDefinition),
|
||||
background: Schema.optional(BackgroundDefinition),
|
||||
border: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
|
||||
scrollbar: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
|
||||
diff: Schema.optional(DiffDefinition),
|
||||
syntax: Schema.optional(SyntaxDefinition),
|
||||
markdown: Schema.optional(MarkdownDefinition),
|
||||
}),
|
||||
),
|
||||
text: Schema.optional(TextDefinition),
|
||||
background: Schema.optional(BackgroundDefinition),
|
||||
border: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
|
||||
scrollbar: Schema.optional(Schema.Struct({ default: Schema.optional(ColorValue) })),
|
||||
diff: Schema.optional(DiffDefinition),
|
||||
syntax: Schema.optional(SyntaxDefinition),
|
||||
markdown: Schema.optional(MarkdownDefinition),
|
||||
})
|
||||
export type ThemeTokensDefinition = Schema.Schema.Type<typeof ThemeTokensDefinition>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ActionVariant,
|
||||
BaseHue,
|
||||
FeedbackKind,
|
||||
FormfieldState,
|
||||
HueAlias,
|
||||
HueStep,
|
||||
MarkdownToken,
|
||||
@@ -12,43 +13,49 @@ import type {
|
||||
} from "./schema"
|
||||
|
||||
export type ResolvedActionState = "default" | ActionState
|
||||
export type ResolvedFormfieldState = "default" | FormfieldState
|
||||
export type HueScale = Readonly<Record<HueStep, RGBA>>
|
||||
export type Hue = Readonly<Record<BaseHue | HueAlias, HueScale>>
|
||||
export type StatefulColor = Readonly<Record<ResolvedActionState, RGBA>>
|
||||
export type FormfieldColor = Readonly<Record<ResolvedFormfieldState, RGBA>>
|
||||
|
||||
export type ResolvedThemeView = {
|
||||
readonly hue: Hue
|
||||
readonly color: {
|
||||
readonly text: {
|
||||
readonly default: RGBA
|
||||
readonly subdued: RGBA
|
||||
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
|
||||
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA; readonly subdued: RGBA }>>
|
||||
}
|
||||
readonly background: {
|
||||
readonly default: RGBA
|
||||
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
|
||||
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA }>>
|
||||
}
|
||||
readonly border: { readonly default: RGBA }
|
||||
readonly scrollbar: { readonly default: RGBA }
|
||||
readonly diff: {
|
||||
readonly text: {
|
||||
readonly added: RGBA
|
||||
readonly removed: RGBA
|
||||
readonly context: RGBA
|
||||
readonly hunkHeader: RGBA
|
||||
}
|
||||
readonly background: { readonly added: RGBA; readonly removed: RGBA; readonly context: RGBA }
|
||||
readonly highlight: { readonly added: RGBA; readonly removed: RGBA }
|
||||
readonly lineNumber: {
|
||||
readonly text: RGBA
|
||||
readonly background: { readonly added: RGBA; readonly removed: RGBA }
|
||||
}
|
||||
}
|
||||
readonly syntax: Readonly<Record<SyntaxToken, RGBA>>
|
||||
readonly markdown: Readonly<Record<MarkdownToken, RGBA>>
|
||||
readonly text: {
|
||||
readonly default: RGBA
|
||||
readonly subdued: RGBA
|
||||
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
|
||||
readonly formfield: FormfieldColor
|
||||
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA; readonly subdued: RGBA }>>
|
||||
}
|
||||
readonly background: {
|
||||
readonly default: RGBA
|
||||
readonly surface: {
|
||||
readonly offset: RGBA
|
||||
readonly overlay: RGBA
|
||||
}
|
||||
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
|
||||
readonly formfield: FormfieldColor
|
||||
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA }>>
|
||||
}
|
||||
readonly border: { readonly default: RGBA }
|
||||
readonly scrollbar: { readonly default: RGBA }
|
||||
readonly diff: {
|
||||
readonly text: {
|
||||
readonly added: RGBA
|
||||
readonly removed: RGBA
|
||||
readonly context: RGBA
|
||||
readonly hunkHeader: RGBA
|
||||
}
|
||||
readonly background: { readonly added: RGBA; readonly removed: RGBA; readonly context: RGBA }
|
||||
readonly highlight: { readonly added: RGBA; readonly removed: RGBA }
|
||||
readonly lineNumber: {
|
||||
readonly text: RGBA
|
||||
readonly background: { readonly added: RGBA; readonly removed: RGBA }
|
||||
}
|
||||
}
|
||||
readonly syntax: Readonly<Record<SyntaxToken, RGBA>>
|
||||
readonly markdown: Readonly<Record<MarkdownToken, RGBA>>
|
||||
}
|
||||
|
||||
export type ResolvedTheme = ResolvedThemeView & {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RGBA } from "@opentui/core"
|
||||
import type { Theme, ThemeJson } from "../index"
|
||||
import { DEFAULT_THEME } from "./defaults"
|
||||
import type { ThemeFile } from "./index"
|
||||
|
||||
type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItemText">
|
||||
@@ -7,109 +8,141 @@ type ThemeColor = Exclude<keyof Theme, "thinkingOpacity" | "_hasSelectedListItem
|
||||
export function migrateV1(theme: ThemeJson): ThemeFile {
|
||||
return {
|
||||
version: 2,
|
||||
light: migrateMode(resolveV1(theme, "light")),
|
||||
dark: migrateMode(resolveV1(theme, "dark")),
|
||||
standalone: true,
|
||||
light: migrateMode(resolveV1(theme, "light"), "light"),
|
||||
dark: migrateMode(resolveV1(theme, "dark"), "dark"),
|
||||
}
|
||||
}
|
||||
|
||||
function migrateMode(theme: Theme): ThemeFile["light"] {
|
||||
function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] {
|
||||
const color = (key: ThemeColor) => hex(theme[key])
|
||||
const selected = hex(selectedForeground(theme, theme.primary))
|
||||
const destructive = hex(selectedForeground(theme, theme.error))
|
||||
|
||||
return {
|
||||
hue: { accent: accentScale(theme.accent) },
|
||||
color: {
|
||||
text: {
|
||||
hue: {
|
||||
...DEFAULT_THEME[mode].hue,
|
||||
accent: hueScale(theme.secondary),
|
||||
},
|
||||
text: {
|
||||
default: color("text"),
|
||||
subdued: color("textMuted"),
|
||||
action: {
|
||||
primary: {
|
||||
default: color("text"),
|
||||
$disabled: color("textMuted"),
|
||||
$focused: selected,
|
||||
},
|
||||
secondary: {
|
||||
default: "$text.default",
|
||||
$disabled: color("textMuted"),
|
||||
},
|
||||
destructive: { default: destructive, $disabled: color("textMuted") },
|
||||
},
|
||||
formfield: {
|
||||
default: color("text"),
|
||||
subdued: color("textMuted"),
|
||||
action: {
|
||||
primary: { default: selected },
|
||||
secondary: {
|
||||
default: color("text"),
|
||||
$hovered: color("text"),
|
||||
$pressed: color("text"),
|
||||
$selected: color("text"),
|
||||
$focused: color("text"),
|
||||
$disabled: color("textMuted"),
|
||||
},
|
||||
destructive: { default: destructive },
|
||||
},
|
||||
feedback: {
|
||||
error: { default: color("error") },
|
||||
warning: { default: color("warning") },
|
||||
success: { default: color("success") },
|
||||
info: { default: color("info") },
|
||||
},
|
||||
$focused: color("primary"),
|
||||
$pressed: color("primary"),
|
||||
$disabled: color("textMuted"),
|
||||
$selected: color("primary"),
|
||||
},
|
||||
background: {
|
||||
default: color("background"),
|
||||
action: {
|
||||
primary: { default: color("primary") },
|
||||
secondary: {
|
||||
default: color("backgroundMenu"),
|
||||
$hovered: color("backgroundElement"),
|
||||
$pressed: color("backgroundElement"),
|
||||
$selected: color("backgroundMenu"),
|
||||
$focused: color("backgroundElement"),
|
||||
$disabled: color("backgroundMenu"),
|
||||
},
|
||||
destructive: { default: color("error") },
|
||||
},
|
||||
},
|
||||
border: { default: color("border") },
|
||||
scrollbar: { default: color("borderActive") },
|
||||
diff: {
|
||||
text: {
|
||||
added: color("diffAdded"),
|
||||
removed: color("diffRemoved"),
|
||||
context: color("diffContext"),
|
||||
hunkHeader: color("diffHunkHeader"),
|
||||
},
|
||||
background: {
|
||||
added: color("diffAddedBg"),
|
||||
removed: color("diffRemovedBg"),
|
||||
context: color("diffContextBg"),
|
||||
},
|
||||
highlight: { added: color("diffHighlightAdded"), removed: color("diffHighlightRemoved") },
|
||||
lineNumber: {
|
||||
text: color("diffLineNumber"),
|
||||
background: {
|
||||
added: color("diffAddedLineNumberBg"),
|
||||
removed: color("diffRemovedLineNumberBg"),
|
||||
},
|
||||
},
|
||||
},
|
||||
syntax: {
|
||||
comment: color("syntaxComment"),
|
||||
keyword: color("syntaxKeyword"),
|
||||
function: color("syntaxFunction"),
|
||||
variable: color("syntaxVariable"),
|
||||
string: color("syntaxString"),
|
||||
number: color("syntaxNumber"),
|
||||
type: color("syntaxType"),
|
||||
operator: color("syntaxOperator"),
|
||||
punctuation: color("syntaxPunctuation"),
|
||||
},
|
||||
markdown: {
|
||||
text: color("markdownText"),
|
||||
heading: color("markdownHeading"),
|
||||
link: color("markdownLink"),
|
||||
linkText: color("markdownLinkText"),
|
||||
code: color("markdownCode"),
|
||||
blockQuote: color("markdownBlockQuote"),
|
||||
emphasis: color("markdownEmph"),
|
||||
strong: color("markdownStrong"),
|
||||
horizontalRule: color("markdownHorizontalRule"),
|
||||
listItem: color("markdownListItem"),
|
||||
listEnumeration: color("markdownListEnumeration"),
|
||||
image: color("markdownImage"),
|
||||
imageText: color("markdownImageText"),
|
||||
codeBlock: color("markdownCodeBlock"),
|
||||
feedback: {
|
||||
error: { default: color("error") },
|
||||
warning: { default: color("warning") },
|
||||
success: { default: color("success") },
|
||||
info: { default: color("info") },
|
||||
},
|
||||
},
|
||||
"@context:elevated": { color: { background: { default: color("backgroundPanel") } } },
|
||||
"@context:overlay": { color: { background: { default: color("backgroundMenu") } } },
|
||||
background: {
|
||||
default: color("background"),
|
||||
surface: {
|
||||
offset: color("backgroundPanel"),
|
||||
overlay: color("backgroundMenu"),
|
||||
},
|
||||
action: {
|
||||
primary: { default: color("background"), $focused: color("primary") },
|
||||
secondary: {
|
||||
default: "$background.default",
|
||||
$focused: color("backgroundElement"),
|
||||
$pressed: color("backgroundElement"),
|
||||
},
|
||||
destructive: { default: color("error") },
|
||||
},
|
||||
formfield: {
|
||||
default: "$background.default",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$background.default" },
|
||||
warning: { default: "$background.default" },
|
||||
success: { default: "$background.default" },
|
||||
info: { default: "$background.default" },
|
||||
},
|
||||
},
|
||||
border: { default: color("border") },
|
||||
scrollbar: { default: color("borderActive") },
|
||||
diff: {
|
||||
text: {
|
||||
added: color("diffAdded"),
|
||||
removed: color("diffRemoved"),
|
||||
context: color("diffContext"),
|
||||
hunkHeader: color("diffHunkHeader"),
|
||||
},
|
||||
background: {
|
||||
added: color("diffAddedBg"),
|
||||
removed: color("diffRemovedBg"),
|
||||
context: color("diffContextBg"),
|
||||
},
|
||||
highlight: { added: color("diffHighlightAdded"), removed: color("diffHighlightRemoved") },
|
||||
lineNumber: {
|
||||
text: color("diffLineNumber"),
|
||||
background: {
|
||||
added: color("diffAddedLineNumberBg"),
|
||||
removed: color("diffRemovedLineNumberBg"),
|
||||
},
|
||||
},
|
||||
},
|
||||
syntax: {
|
||||
comment: color("syntaxComment"),
|
||||
keyword: color("syntaxKeyword"),
|
||||
function: color("syntaxFunction"),
|
||||
variable: color("syntaxVariable"),
|
||||
string: color("syntaxString"),
|
||||
number: color("syntaxNumber"),
|
||||
type: color("syntaxType"),
|
||||
operator: color("syntaxOperator"),
|
||||
punctuation: color("syntaxPunctuation"),
|
||||
},
|
||||
markdown: {
|
||||
text: color("markdownText"),
|
||||
heading: color("markdownHeading"),
|
||||
link: color("markdownLink"),
|
||||
linkText: color("markdownLinkText"),
|
||||
code: color("markdownCode"),
|
||||
blockQuote: color("markdownBlockQuote"),
|
||||
emphasis: color("markdownEmph"),
|
||||
strong: color("markdownStrong"),
|
||||
horizontalRule: color("markdownHorizontalRule"),
|
||||
listItem: color("markdownListItem"),
|
||||
listEnumeration: color("markdownListEnumeration"),
|
||||
image: color("markdownImage"),
|
||||
imageText: color("markdownImageText"),
|
||||
codeBlock: color("markdownCodeBlock"),
|
||||
},
|
||||
"@context:elevated": {
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: {
|
||||
primary: {
|
||||
default: "$background.surface.offset",
|
||||
$focused: color("primary"),
|
||||
},
|
||||
secondary: {
|
||||
default: "$background.surface.offset",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"@context:overlay": { background: { default: "$background.surface.overlay" } },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,17 +192,17 @@ function selectedForeground(theme: Theme, background: RGBA) {
|
||||
: RGBA.fromInts(255, 255, 255)
|
||||
}
|
||||
|
||||
function accentScale(accent: RGBA) {
|
||||
function hueScale(color: RGBA) {
|
||||
return {
|
||||
100: mix(accent, 255, 0.66),
|
||||
200: mix(accent, 255, 0.33),
|
||||
300: hex(accent),
|
||||
400: mix(accent, 0, 0.1),
|
||||
500: mix(accent, 0, 0.2),
|
||||
600: mix(accent, 0, 0.3),
|
||||
700: mix(accent, 0, 0.4),
|
||||
800: mix(accent, 0, 0.5),
|
||||
900: mix(accent, 0, 0.6),
|
||||
100: mix(color, 255, 0.8),
|
||||
200: mix(color, 255, 0.6),
|
||||
300: mix(color, 255, 0.4),
|
||||
400: mix(color, 255, 0.2),
|
||||
500: hex(color),
|
||||
600: mix(color, 0, 0.15),
|
||||
700: mix(color, 0, 0.3),
|
||||
800: mix(color, 0, 0.45),
|
||||
900: mix(color, 0, 0.6),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +228,22 @@ function hexInts(r: number, g: number, b: number, a: number) {
|
||||
function ansi(code: number) {
|
||||
if (code < 16) {
|
||||
const colors = [
|
||||
"#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0",
|
||||
"#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff",
|
||||
"#000000",
|
||||
"#800000",
|
||||
"#008000",
|
||||
"#808000",
|
||||
"#000080",
|
||||
"#800080",
|
||||
"#008080",
|
||||
"#c0c0c0",
|
||||
"#808080",
|
||||
"#ff0000",
|
||||
"#00ff00",
|
||||
"#ffff00",
|
||||
"#0000ff",
|
||||
"#ff00ff",
|
||||
"#00ffff",
|
||||
"#ffffff",
|
||||
]
|
||||
return RGBA.fromHex(colors[code] ?? "#000000")
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type ToastInput = Omit<ToastOptions, "duration"> & { duration?: number }
|
||||
|
||||
export function Toast() {
|
||||
const toast = useToast()
|
||||
const { theme } = useTheme()
|
||||
const { theme, themeV2 } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
||||
return (
|
||||
@@ -37,11 +37,11 @@ export function Toast() {
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<Show when={current().title}>
|
||||
<text attributes={TextAttributes.BOLD} marginBottom={1} fg={theme.text}>
|
||||
<text attributes={TextAttributes.BOLD} marginBottom={1} fg={themeV2.text()}>
|
||||
{current().title}
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text} wrapMode="word" width="100%">
|
||||
<text fg={themeV2.text()} wrapMode="word" width="100%">
|
||||
{current().message}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -15,6 +15,7 @@ test("resolves nested config and keybind defaults", () => {
|
||||
leader: { timeout: 500 },
|
||||
scroll: { speed: 2, acceleration: true },
|
||||
diffs: { view: "split" },
|
||||
debug: { devtools: true },
|
||||
},
|
||||
{ terminalSuspend: true },
|
||||
)
|
||||
@@ -23,6 +24,7 @@ test("resolves nested config and keybind defaults", () => {
|
||||
expect(config.keybinds.get("leader")?.[0]?.key).toBe("ctrl+o")
|
||||
expect(config.scroll).toEqual({ speed: 2, acceleration: true })
|
||||
expect(config.diffs).toEqual({ view: "split" })
|
||||
expect(config.debug).toEqual({ devtools: true })
|
||||
})
|
||||
|
||||
test("provides config and its host interface", async () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { DevTools } from "../src/devtools"
|
||||
|
||||
test("registers and updates grouped DevTools data", () => {
|
||||
const group = DevTools.register({ id: "test", title: "Test data" })
|
||||
|
||||
group.set("Duration", "1.00 ms")
|
||||
group.set("Duration", "2.00 ms")
|
||||
group.set("Count", 2)
|
||||
|
||||
expect(DevTools.data().find((item) => item.id === "test")).toEqual({
|
||||
id: "test",
|
||||
title: "Test data",
|
||||
entries: [
|
||||
{ key: "Duration", value: "2.00 ms" },
|
||||
{ key: "Count", value: 2 },
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -14,22 +14,27 @@ test("provides reactive property, variant, state, and context accessors", () =>
|
||||
return key ? resolved().contexts[key] ?? resolved() : resolved()
|
||||
})
|
||||
|
||||
expect(theme.color.text()).toBe(resolved().color.text.default)
|
||||
expect(theme.color.text.subdued()).toBe(resolved().color.text.subdued)
|
||||
expect(theme.color.text.action()).toBe(resolved().color.text.action.primary.default)
|
||||
expect(theme.color.text.action.primary("pressed")).toBe(resolved().color.text.action.primary.pressed)
|
||||
expect(theme.color.background.action.secondary("disabled")).toBe(
|
||||
resolved().color.background.action.secondary.disabled,
|
||||
expect(theme.text()).toBe(resolved().text.default)
|
||||
expect(theme.text.subdued()).toBe(resolved().text.subdued)
|
||||
expect(theme.text.action()).toBe(resolved().text.action.primary.default)
|
||||
expect(theme.text.action.primary("pressed")).toBe(resolved().text.action.primary.pressed)
|
||||
expect(theme.background.action.secondary("disabled")).toBe(
|
||||
resolved().background.action.secondary.disabled,
|
||||
)
|
||||
expect(theme.color.scrollbar()).toBe(resolved().color.scrollbar.default)
|
||||
expect(theme.color.diff.text.added()).toBe(resolved().color.diff.text.added)
|
||||
expect(theme.background.surface.offset()).toBe(resolved().background.surface.offset)
|
||||
expect(theme.background.surface.overlay()).toBe(resolved().background.surface.overlay)
|
||||
expect(theme.scrollbar()).toBe(resolved().scrollbar.default)
|
||||
expect(theme.diff.text.added()).toBe(resolved().diff.text.added)
|
||||
|
||||
setContext("@context:elevated")
|
||||
expect(theme.color.text()).toBe(resolved().contexts["@context:elevated"]!.color.text.default)
|
||||
expect(theme.color.background.action.primary("selected")).toBe(
|
||||
resolved().contexts["@context:elevated"]!.color.background.action.primary.selected,
|
||||
expect(theme.text()).toBe(resolved().contexts["@context:elevated"]!.text.default)
|
||||
expect(theme.background.action.primary("focused")).toBe(
|
||||
resolved().contexts["@context:elevated"]!.background.action.primary.focused,
|
||||
)
|
||||
expect(theme.background.formfield("selected")).toBe(
|
||||
resolved().contexts["@context:elevated"]!.background.formfield.selected,
|
||||
)
|
||||
|
||||
setResolved(resolveTheme(selectTheme(DEFAULT_THEME, "dark")))
|
||||
expect(theme.color.text()).toBe(resolved().contexts["@context:elevated"]!.color.text.default)
|
||||
expect(theme.text()).toBe(resolved().contexts["@context:elevated"]!.text.default)
|
||||
})
|
||||
|
||||
@@ -14,32 +14,36 @@ test("resolves independent definitions and hue aliases", () => {
|
||||
|
||||
expect(lightTheme.hue.accent).toBe(lightTheme.hue.blue)
|
||||
expect(lightTheme.hue.neutral).toBe(lightTheme.hue.gray)
|
||||
expect(lightTheme.color.text.default).toBeInstanceOf(RGBA)
|
||||
expect(darkTheme.color.background.default).toBeInstanceOf(RGBA)
|
||||
expect(lightTheme.color.syntax.keyword).toBeInstanceOf(RGBA)
|
||||
expect(lightTheme.color.text.action.primary.default).toBe(lightTheme.hue.neutral[100])
|
||||
expect(lightTheme.contexts["@context:elevated"]?.color.background.action.primary.default).toBe(
|
||||
expect(lightTheme.text.default).toBeInstanceOf(RGBA)
|
||||
expect(darkTheme.background.default).toBeInstanceOf(RGBA)
|
||||
expect(lightTheme.background.surface.offset).toBe(lightTheme.hue.neutral[200])
|
||||
expect(lightTheme.background.surface.overlay).toBe(lightTheme.hue.neutral[300])
|
||||
expect(lightTheme.syntax.keyword).toBeInstanceOf(RGBA)
|
||||
expect(lightTheme.text.action.primary.default).toBe(lightTheme.hue.neutral[100])
|
||||
expect(lightTheme.contexts["@context:elevated"]?.background.action.primary.default).toBe(
|
||||
lightTheme.hue.accent[500],
|
||||
)
|
||||
expect(lightTheme.contexts["@context:elevated"]?.color.text.action.primary.default).toBe(
|
||||
expect(lightTheme.contexts["@context:elevated"]?.background.default).toBe(lightTheme.background.surface.offset)
|
||||
expect(lightTheme.contexts["@context:elevated"]?.text.action.primary.default).toBe(
|
||||
lightTheme.hue.neutral[100],
|
||||
)
|
||||
expect(lightTheme.contexts["@context:overlay"]?.color.background.action.primary.default).toBe(
|
||||
expect(lightTheme.contexts["@context:overlay"]?.background.action.primary.default).toBe(
|
||||
lightTheme.hue.accent[500],
|
||||
)
|
||||
expect(lightTheme.contexts["@context:overlay"]?.color.text.action.primary.default).toBe(
|
||||
expect(lightTheme.contexts["@context:overlay"]?.background.default).toBe(lightTheme.background.surface.overlay)
|
||||
expect(lightTheme.contexts["@context:overlay"]?.text.action.primary.default).toBe(
|
||||
lightTheme.hue.neutral[100],
|
||||
)
|
||||
expect(darkTheme.contexts["@context:elevated"]?.color.background.action.primary.default).toBe(
|
||||
expect(darkTheme.contexts["@context:elevated"]?.background.action.primary.default).toBe(
|
||||
darkTheme.hue.accent[400],
|
||||
)
|
||||
expect(darkTheme.contexts["@context:elevated"]?.color.text.action.primary.default).toBe(
|
||||
expect(darkTheme.contexts["@context:elevated"]?.text.action.primary.default).toBe(
|
||||
darkTheme.hue.neutral[100],
|
||||
)
|
||||
expect(darkTheme.contexts["@context:overlay"]?.color.background.action.primary.default).toBe(
|
||||
expect(darkTheme.contexts["@context:overlay"]?.background.action.primary.default).toBe(
|
||||
darkTheme.hue.accent[400],
|
||||
)
|
||||
expect(darkTheme.contexts["@context:overlay"]?.color.text.action.primary.default).toBe(
|
||||
expect(darkTheme.contexts["@context:overlay"]?.text.action.primary.default).toBe(
|
||||
darkTheme.hue.neutral[900],
|
||||
)
|
||||
})
|
||||
@@ -50,16 +54,16 @@ test("merges partial files with the selected OpenCode defaults", () => {
|
||||
version: 2,
|
||||
light: {
|
||||
hue: light.hue,
|
||||
color: { text: { default: "#123456" } },
|
||||
text: { default: "#123456" },
|
||||
},
|
||||
dark: { hue: dark.hue },
|
||||
},
|
||||
"light",
|
||||
)
|
||||
|
||||
expect(theme.color.text.default.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(theme.color.text.subdued.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(theme.color.background.action.destructive.pressed).toBeInstanceOf(RGBA)
|
||||
expect(theme.text.default.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(theme.text.subdued.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(theme.background.action.destructive.pressed).toBeInstanceOf(RGBA)
|
||||
})
|
||||
|
||||
test("expands user structural fallbacks before merging defaults", () => {
|
||||
@@ -68,7 +72,7 @@ test("expands user structural fallbacks before merging defaults", () => {
|
||||
version: 2,
|
||||
light: {
|
||||
hue: light.hue,
|
||||
color: { background: { action: { primary: { default: "#123456" } } } },
|
||||
background: { action: { primary: { default: "#123456" } } },
|
||||
},
|
||||
dark: { hue: dark.hue },
|
||||
},
|
||||
@@ -79,17 +83,17 @@ test("expands user structural fallbacks before merging defaults", () => {
|
||||
version: 2,
|
||||
light: {
|
||||
hue: light.hue,
|
||||
color: { background: { action: { primary: { $pressed: "#654321" } } } },
|
||||
background: { action: { primary: { $pressed: "#654321" } } },
|
||||
},
|
||||
dark: { hue: dark.hue },
|
||||
},
|
||||
"light",
|
||||
)
|
||||
|
||||
expect(expanded.color.background.action.primary.pressed.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(isolatedState.color.background.action.primary.pressed.toInts()).toEqual([101, 67, 33, 255])
|
||||
expect(isolatedState.color.background.action.primary.hovered.toInts()).toEqual(
|
||||
resolveTheme(light).color.background.action.primary.hovered.toInts(),
|
||||
expect(expanded.background.action.primary.pressed.toInts()).toEqual([18, 52, 86, 255])
|
||||
expect(isolatedState.background.action.primary.pressed.toInts()).toEqual([101, 67, 33, 255])
|
||||
expect(isolatedState.background.action.primary.focused.toInts()).toEqual(
|
||||
resolveTheme(light).background.action.primary.focused.toInts(),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -98,70 +102,89 @@ test("standalone themes skip OpenCode defaults and use the red core fallback", (
|
||||
const lightTheme = resolveThemeFile(file, "light")
|
||||
const darkTheme = resolveThemeFile(file, "dark")
|
||||
|
||||
expect(lightTheme.color.text.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(lightTheme.color.background.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(darkTheme.color.text.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(darkTheme.color.background.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(lightTheme.text.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(lightTheme.background.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(darkTheme.text.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
expect(darkTheme.background.default.toInts()).toEqual([255, 0, 0, 255])
|
||||
})
|
||||
|
||||
test("uses defaults for the selected mode when it merges the other mode", () => {
|
||||
const theme = resolveThemeFile({ version: 2, light: { hue: light.hue }, dark: { mergeMode: true } }, "dark")
|
||||
expect(theme.color.background.default.toInts()).toEqual(resolveTheme(dark).color.background.default.toInts())
|
||||
expect(theme.background.default.toInts()).toEqual(resolveTheme(dark).background.default.toInts())
|
||||
})
|
||||
|
||||
test("resolves matched action variants and states", () => {
|
||||
const theme = resolveTheme(light)
|
||||
|
||||
expect(theme.color.text.action.primary.pressed).toBeInstanceOf(RGBA)
|
||||
expect(theme.color.background.action.primary.pressed).toBeInstanceOf(RGBA)
|
||||
expect(theme.color.text.action.secondary.default).toBeInstanceOf(RGBA)
|
||||
expect(theme.color.background.action.destructive.disabled).toBeInstanceOf(RGBA)
|
||||
expect(theme.text.action.primary.pressed).toBeInstanceOf(RGBA)
|
||||
expect(theme.background.action.primary.pressed).toBeInstanceOf(RGBA)
|
||||
expect(theme.text.action.secondary.default).toBeInstanceOf(RGBA)
|
||||
expect(theme.background.action.destructive.disabled).toBeInstanceOf(RGBA)
|
||||
})
|
||||
|
||||
test("resolves transparent colors", () => {
|
||||
const theme = resolveThemeFile({
|
||||
version: 2,
|
||||
light: { background: { formfield: { default: "transparent" } } },
|
||||
dark: { background: { formfield: { default: "transparent" } } },
|
||||
})
|
||||
expect(theme.background.formfield.default.toInts()).toEqual([0, 0, 0, 0])
|
||||
})
|
||||
|
||||
test("reports theme decoding failures as native errors", () => {
|
||||
expect(() =>
|
||||
resolveThemeFile(
|
||||
{
|
||||
version: 2,
|
||||
light: { text: { default: "opaque" } },
|
||||
dark: {},
|
||||
} as never,
|
||||
"light",
|
||||
"custom",
|
||||
),
|
||||
).toThrow('Invalid theme: custom "opaque" is an invalid value')
|
||||
})
|
||||
|
||||
test("context overrides rewire semantic references and apply state precedence", () => {
|
||||
const definition = override(light, {
|
||||
color: {
|
||||
text: {
|
||||
default: "#111111",
|
||||
action: {
|
||||
primary: { default: "$color.text.default", $pressed: "#222222" },
|
||||
secondary: { default: "$color.text.default" },
|
||||
},
|
||||
text: {
|
||||
default: "#111111",
|
||||
action: {
|
||||
primary: { default: "$text.default", $pressed: "#222222" },
|
||||
secondary: { default: "$text.default" },
|
||||
},
|
||||
},
|
||||
"@context:elevated": {
|
||||
color: {
|
||||
text: {
|
||||
default: "#333333",
|
||||
action: { primary: { default: "#444444", $selected: "#555555" } },
|
||||
},
|
||||
text: {
|
||||
default: "#333333",
|
||||
action: { primary: { default: "#444444", $focused: "#555555" } },
|
||||
},
|
||||
},
|
||||
})
|
||||
const theme = resolveTheme(definition)
|
||||
const overlay = theme.contexts["@context:elevated"]!
|
||||
|
||||
expect(overlay.color.text.default.toInts()).toEqual([51, 51, 51, 255])
|
||||
expect(overlay.color.text.action.secondary.default.toInts()).toEqual([51, 51, 51, 255])
|
||||
expect(overlay.color.text.action.primary.pressed.toInts()).toEqual([68, 68, 68, 255])
|
||||
expect(overlay.color.text.action.primary.selected.toInts()).toEqual([85, 85, 85, 255])
|
||||
expect(overlay.text.default.toInts()).toEqual([51, 51, 51, 255])
|
||||
expect(overlay.text.action.secondary.default.toInts()).toEqual([51, 51, 51, 255])
|
||||
expect(overlay.text.action.primary.pressed.toInts()).toEqual([68, 68, 68, 255])
|
||||
expect(overlay.text.action.primary.focused.toInts()).toEqual([85, 85, 85, 255])
|
||||
})
|
||||
|
||||
test("rejects missing, base, and contextual reference cycles", () => {
|
||||
expect(() => resolveTheme(override(light, { color: { text: { default: "$missing.color" } } }))).toThrow(
|
||||
'Theme reference "$missing.color" was not found',
|
||||
expect(() => resolveTheme(override(light, { text: { default: "$missing" } }))).toThrow(
|
||||
'Theme reference "$missing" was not found',
|
||||
)
|
||||
expect(() =>
|
||||
resolveTheme(
|
||||
override(light, {
|
||||
color: { text: { default: "$color.text.subdued", subdued: "$color.text.default" } },
|
||||
text: { default: "$text.subdued", subdued: "$text.default" },
|
||||
}),
|
||||
),
|
||||
).toThrow("Circular theme reference")
|
||||
expect(() =>
|
||||
resolveTheme(
|
||||
override(light, {
|
||||
"@context:elevated": { color: { text: { default: "$color.text.default" } } },
|
||||
"@context:elevated": { text: { default: "$text.default" } },
|
||||
}),
|
||||
),
|
||||
).toThrow("Circular theme reference")
|
||||
@@ -179,9 +202,9 @@ test("validates complete hues, resolved groups, and hue-only syntax", () => {
|
||||
expect(() =>
|
||||
resolveTheme({
|
||||
...light,
|
||||
color: { ...light.color, syntax: { ...light.color?.syntax, keyword: "$color.text.default" } },
|
||||
syntax: { ...light.syntax, keyword: "$text.default" },
|
||||
} as unknown as ThemeDefinition),
|
||||
).toThrow("$color.text.default")
|
||||
).toThrow("$text.default")
|
||||
})
|
||||
|
||||
function override(base: ThemeDefinition, value: Partial<ThemeDefinition>) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { HueDefinition, ThemeDefinition, ThemeFile } from "../../../src/the
|
||||
import { selectTheme, selectThemeMode } from "../../../src/theme/v2/select"
|
||||
|
||||
const hue = {} as HueDefinition
|
||||
const light = { hue, color: { text: { default: "#111111", subdued: "#222222" } } } satisfies ThemeDefinition
|
||||
const dark = { hue, color: { text: { default: "#eeeeee", subdued: "#dddddd" } } } satisfies ThemeDefinition
|
||||
const light = { hue, text: { default: "#111111", subdued: "#222222" } } satisfies ThemeDefinition
|
||||
const dark = { hue, text: { default: "#eeeeee", subdued: "#dddddd" } } satisfies ThemeDefinition
|
||||
|
||||
test("requires and selects independent light and dark themes", () => {
|
||||
const file = { version: 2, light, dark } satisfies ThemeFile
|
||||
@@ -18,13 +18,13 @@ test("merges an expanded mode override over the other mode", () => {
|
||||
const file = {
|
||||
version: 2,
|
||||
light,
|
||||
dark: { mergeMode: true, color: { text: { default: "#ffffff" } } },
|
||||
dark: { mergeMode: true, text: { default: "#ffffff" } },
|
||||
} satisfies ThemeFile
|
||||
const selected = selectTheme(file, "dark")
|
||||
|
||||
expect(selected.hue).toBeDefined()
|
||||
expect(selected.color?.text?.default).toBe("#ffffff")
|
||||
expect(selected.color?.text?.subdued).toBe("$color.text.default")
|
||||
expect(selected.text?.default).toBe("#ffffff")
|
||||
expect(selected.text?.subdued).toBe("$text.default")
|
||||
})
|
||||
|
||||
test("rejects mutual mode merging", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ const text = {
|
||||
secondary: { default: "$hue.neutral.900" },
|
||||
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
|
||||
},
|
||||
formfield: { default: "$hue.neutral.600", $selected: "$hue.neutral.100" },
|
||||
feedback: {
|
||||
error: { default: "$hue.red.700", subdued: "$hue.red.600" },
|
||||
},
|
||||
@@ -16,32 +17,36 @@ const text = {
|
||||
|
||||
const background = {
|
||||
default: "$hue.neutral.100",
|
||||
surface: { offset: "$hue.neutral.200", overlay: "$hue.neutral.300" },
|
||||
action: {
|
||||
primary: { default: "$hue.accent.600", $pressed: "$hue.accent.800" },
|
||||
secondary: { default: "$hue.neutral.200" },
|
||||
destructive: { default: "$hue.red.600" },
|
||||
},
|
||||
formfield: { default: "$hue.neutral.100", $selected: "$hue.accent.600" },
|
||||
feedback: { error: { default: "$hue.red.100" } },
|
||||
} satisfies BackgroundDefinition
|
||||
|
||||
const definition = {
|
||||
hue: {} as ThemeDefinition["hue"],
|
||||
color: { text, background, border: { default: "$hue.neutral.300" } },
|
||||
text,
|
||||
background,
|
||||
border: { default: "$hue.neutral.300" },
|
||||
"@context:elevated": {
|
||||
color: {
|
||||
text: { default: "$hue.neutral.800" },
|
||||
background: { default: "$hue.neutral.200" },
|
||||
},
|
||||
text: { default: "$hue.neutral.800" },
|
||||
background: { default: "$hue.neutral.200" },
|
||||
},
|
||||
"@context:overlay": { color: { background: { default: "$hue.neutral.300" } } },
|
||||
"@context:overlay": { background: { default: "$hue.neutral.300" } },
|
||||
} satisfies ThemeDefinition
|
||||
|
||||
const file = { version: 2, light: definition, dark: definition } satisfies ThemeFile
|
||||
|
||||
test("supports property-first definitions, variants, states, and contexts", () => {
|
||||
expect(text.action.primary.$pressed).toBe("$hue.neutral.200")
|
||||
expect(text.formfield.$selected).toBe("$hue.neutral.100")
|
||||
expect(background.action.destructive.default).toBe("$hue.red.600")
|
||||
expect(definition["@context:elevated"].color?.text?.default).toBe("$hue.neutral.800")
|
||||
expect(definition["@context:overlay"].color?.background?.default).toBe("$hue.neutral.300")
|
||||
expect(background.surface.offset).toBe("$hue.neutral.200")
|
||||
expect(definition["@context:elevated"].text?.default).toBe("$hue.neutral.800")
|
||||
expect(definition["@context:overlay"].background?.default).toBe("$hue.neutral.300")
|
||||
expect(file.light).toBe(definition)
|
||||
})
|
||||
|
||||
@@ -8,24 +8,47 @@ test("migrates resolved V1 modes into literal V2 tokens", () => {
|
||||
const legacy = resolveV1(DEFAULT_THEMES.opencode, "light")
|
||||
const resolved = resolveThemeFile(migrated, "light")
|
||||
|
||||
expect(migrated.standalone).toBeUndefined()
|
||||
expect(migrated.standalone).toBeTrue()
|
||||
expect(migrated.light.hue?.accent).toBeObject()
|
||||
if (typeof migrated.light.hue?.accent !== "object") throw new Error("Expected a concrete accent scale")
|
||||
expect(migrated.light.hue.accent[300]).toBe(hex(legacy.accent))
|
||||
expect(migrated.light.color?.background?.default).toBe(hex(legacy.background))
|
||||
expect(migrated.light.color?.background?.action?.primary?.default).toBe(hex(legacy.primary))
|
||||
expect(migrated.light.color?.text?.action?.primary?.default).toBe(hex(selectedForeground(legacy, legacy.primary)))
|
||||
expect(migrated.light.color?.scrollbar?.default).toBe(hex(legacy.borderActive))
|
||||
expect(migrated.light.color?.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg))
|
||||
expect(migrated.light.color?.markdown?.emphasis).toBe(hex(legacy.markdownEmph))
|
||||
expect(resolved.color.background.action.secondary.hovered.toInts()).toEqual(legacy.backgroundElement.toInts())
|
||||
expect(resolved.color.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.contexts["@context:elevated"]?.color.background.default.toInts()).toEqual(
|
||||
expect(migrated.light.hue.accent[500]).toBe(hex(legacy.secondary))
|
||||
expect(migrated.light.background?.default).toBe(hex(legacy.background))
|
||||
expect(migrated.light.background?.action?.primary?.default).toBe(hex(legacy.primary))
|
||||
expect(migrated.light.text?.action?.primary?.default).toBe(hex(selectedForeground(legacy, legacy.primary)))
|
||||
expect(migrated.light.scrollbar?.default).toBe(hex(legacy.borderActive))
|
||||
expect(migrated.light.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg))
|
||||
expect(migrated.light.markdown?.emphasis).toBe(hex(legacy.markdownEmph))
|
||||
expect(resolved.background.action.secondary.focused.toInts()).toEqual(legacy.backgroundElement.toInts())
|
||||
expect(resolved.background.surface.offset.toInts()).toEqual(legacy.backgroundPanel.toInts())
|
||||
expect(resolved.background.surface.overlay.toInts()).toEqual(legacy.backgroundMenu.toInts())
|
||||
expect(resolved.background.formfield.selected.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.background.formfield.focused.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.text.formfield.default.toInts()).toEqual(legacy.text.toInts())
|
||||
expect(resolved.text.formfield.selected.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.text.formfield.focused.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.hue.accent[500].toInts()).toEqual(legacy.secondary.toInts())
|
||||
expect(resolved.hue.accent[300].r + resolved.hue.accent[300].g + resolved.hue.accent[300].b).toBeGreaterThan(
|
||||
resolved.hue.accent[500].r + resolved.hue.accent[500].g + resolved.hue.accent[500].b,
|
||||
)
|
||||
expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual(
|
||||
legacy.backgroundPanel.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:overlay"]?.color.background.default.toInts()).toEqual(
|
||||
expect(resolved.contexts["@context:elevated"]?.background.action.secondary.default.toInts()).toEqual(
|
||||
legacy.backgroundPanel.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:elevated"]?.background.action.primary.default.toInts()).toEqual(
|
||||
legacy.primary.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:elevated"]?.text.action.primary.default.toInts()).toEqual(
|
||||
selectedForeground(legacy, legacy.primary).toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:overlay"]?.background.default.toInts()).toEqual(
|
||||
legacy.backgroundMenu.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:overlay"]?.background.action.primary.default.toInts()).toEqual(
|
||||
legacy.primary.toInts(),
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves V1 selected foreground behavior on transparent backgrounds", () => {
|
||||
@@ -35,8 +58,8 @@ test("preserves V1 selected foreground behavior on transparent backgrounds", ()
|
||||
delete source.theme.selectedListItemText
|
||||
const migrated = migrateV1(source)
|
||||
|
||||
expect(migrated.light.color?.text?.action?.primary?.default).toBe("#000000")
|
||||
expect(migrated.dark.color?.text?.action?.primary?.default).toBe("#ffffff")
|
||||
expect(migrated.light.text?.action?.primary?.default).toBe("#000000")
|
||||
expect(migrated.dark.text?.action?.primary?.default).toBe("#ffffff")
|
||||
})
|
||||
|
||||
test("retains V1 circular reference errors", () => {
|
||||
@@ -50,8 +73,8 @@ test("retains V1 circular reference errors", () => {
|
||||
test("migrates every built-in V1 theme in both modes", () => {
|
||||
for (const source of Object.values(DEFAULT_THEMES)) {
|
||||
const migrated = migrateV1(source)
|
||||
expect(resolveThemeFile(migrated, "light").color.text.default).toBeDefined()
|
||||
expect(resolveThemeFile(migrated, "dark").color.text.default).toBeDefined()
|
||||
expect(resolveThemeFile(migrated, "light").text.default).toBeDefined()
|
||||
expect(resolveThemeFile(migrated, "dark").text.default).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# TUI Theme V2 Migration Checklist
|
||||
|
||||
- [x] Add semantic accent foreground and border tokens so components stop
|
||||
reading `hue.accent[300]` directly.
|
||||
- [ ] Add paired badge or label foreground/background tokens to replace V1
|
||||
`secondary` usages.
|
||||
- [ ] Add strong warning and error background treatments with matching readable
|
||||
foregrounds.
|
||||
- [x] Use `text.default` for active cursors, `background.surface.offset` for
|
||||
disabled cursors, and a lighter accent hue for focused form borders.
|
||||
- [x] Add `background.surface.offset` and `background.surface.overlay`, map
|
||||
them from V1 panel/menu backgrounds, and use them as contextual surface
|
||||
defaults.
|
||||
- [ ] Replace `selectedForeground` with complete V2 foreground/background pairs
|
||||
or a V2 contrast helper that supports transparent themes.
|
||||
- [ ] Decide whether thinking opacity is fixed at `0.6` or belongs in a separate
|
||||
presentation-token system.
|
||||
- [ ] Generate syntax styles from resolved V2 tokens, then migrate each UI
|
||||
surface and remove the V1 proxy once no flat V1 color reads remain.
|
||||
Reference in New Issue
Block a user