mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9721223b7e | |||
| 35a626e711 | |||
| bb7b0ff221 | |||
| 68b4038196 | |||
| 3109214900 | |||
| 86ccc3409b | |||
| a89089c88f | |||
| e617c5d689 | |||
| 31983ca5ff | |||
| 59e3b7409f | |||
| b7ce46f7a1 | |||
| 82b8d8fa5d | |||
| 77c837eb1a | |||
| db77cc9845 | |||
| 68043edae6 | |||
| 337681dbbf | |||
| 66afc034d1 | |||
| 11ab8de59f | |||
| 5f074edc3a | |||
| 56b5cdf883 | |||
| fb0e1e4d8d | |||
| b745b1593f | |||
| 7376c3f8e7 | |||
| 831e9bce51 | |||
| 5de73abd82 | |||
| 3adbbc1b23 | |||
| c6c29b3dcf | |||
| a687d7c15f | |||
| 0c6da69f39 | |||
| c4930eb6b2 | |||
| a24549fce7 | |||
| c0f9b13630 | |||
| 98fd53fd5f | |||
| 5b02a3029e | |||
| 94e851c2a2 | |||
| 1658a3ff59 | |||
| 9c8bc64138 | |||
| 80f704ebbf | |||
| 4dae6d1fcf | |||
| 5d2cab39da | |||
| 6963f96d4b | |||
| 05a9e7ce7a | |||
| 896d18ab3f | |||
| 893888536a | |||
| c6221fc8b3 | |||
| ae67f43ff0 | |||
| 76880dce0d | |||
| aafffb5b4b | |||
| a71c9e3f2e | |||
| 0156f03e0e | |||
| e0bb96a9f9 | |||
| 82e5d6d458 | |||
| a4411c21b6 | |||
| 9d61370ac4 | |||
| f3febd6e39 | |||
| f12d55bf1e | |||
| 0c19b71f42 | |||
| 70fa66397e | |||
| 6e8cd3174c | |||
| 5bfffbe083 | |||
| 29d8557d41 | |||
| ffd20b4477 | |||
| 2abaa46e23 | |||
| 0cbbb20d22 | |||
| 81c5e7b9ed | |||
| ddf4897eaa | |||
| 040939fb72 | |||
| f89b83a6d7 | |||
| 82a876da4d | |||
| 69a15ae9c1 | |||
| 18c8e5f451 | |||
| ba3a1cfa0b | |||
| d8563160f7 | |||
| 4a9ff9412e | |||
| d6db6ff198 | |||
| 79c263494f | |||
| 1b5bf32ce5 | |||
| 2e972b3fdc | |||
| d70e9fb01e | |||
| fc082a0f14 | |||
| 953e4e9446 | |||
| 7ea0d37ee3 | |||
| e35d97f9d7 | |||
| 2c0d9a46cb | |||
| 2fe7a7f2d3 | |||
| 8a2f4ddf70 | |||
| 7a94d7a2c5 | |||
| de28fafb47 | |||
| 9d485dd307 | |||
| 613813ac12 | |||
| 7617f59441 |
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
color: "#38A3EE"
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
|
||||
@@ -183,3 +183,5 @@
|
||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
@@ -200,7 +200,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -245,7 +245,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -254,7 +254,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/anthropic": "2.0.56",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/cerebras": "1.0.33",
|
||||
"@ai-sdk/cohere": "2.0.21",
|
||||
@@ -269,7 +269,7 @@
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/perplexity": "2.0.22",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
@@ -347,7 +347,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -367,7 +367,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -378,7 +378,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -391,7 +391,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -403,8 +403,10 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-katex-extension": "5.1.6",
|
||||
"marked-shiki": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
@@ -416,6 +418,7 @@
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/luxon": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
@@ -426,7 +429,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -437,7 +440,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -566,7 +569,7 @@
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
|
||||
|
||||
@@ -1774,6 +1777,8 @@
|
||||
|
||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
|
||||
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
@@ -2786,6 +2791,8 @@
|
||||
|
||||
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
|
||||
|
||||
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
@@ -2876,6 +2883,8 @@
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
||||
|
||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -3898,38 +3907,24 @@
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
|
||||
|
||||
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
|
||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||
@@ -4284,6 +4279,8 @@
|
||||
|
||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -4308,7 +4305,7 @@
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||
|
||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766747458,
|
||||
"narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=",
|
||||
"lastModified": 1766996594,
|
||||
"narHash": "sha256-SosfgQSqVmOkqVgNYJnxW5FvoIQX4grOcpIKNrIwz4o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c633f572eded8c4f3c75b8010129854ed404a6ce",
|
||||
"rev": "0744ef1b047f07d31d9962d757ffe38ec14a4d41",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg="
|
||||
"nodeModules": "sha256-2i/QMBzp9MalOXur36mXaDDU8R9G/0dODCODEQOnaCU="
|
||||
}
|
||||
|
||||
+31
-6
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" style="background-color: var(--background-base)">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -13,14 +13,39 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script">
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
document.documentElement.setAttribute("data-theme", savedTheme)
|
||||
var themeId = localStorage.getItem("opencode-theme-id")
|
||||
if (!themeId) return
|
||||
|
||||
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
var mode = isDark ? "dark" : "light"
|
||||
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
if (themeId === "oc-1") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||
if (css) {
|
||||
var style = document.createElement("style")
|
||||
style.id = "oc-theme-preload"
|
||||
style.textContent =
|
||||
":root{color-scheme:" +
|
||||
mode +
|
||||
";--text-mix-blend-mode:" +
|
||||
(isDark ? "plus-lighter" : "multiply") +
|
||||
";" +
|
||||
css +
|
||||
"}"
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
+45
-42
@@ -8,6 +8,7 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
@@ -45,48 +46,50 @@ export function App() {
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
<ThemeProvider>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
function getFilename(input: string) {
|
||||
const parts = input.split("/")
|
||||
return parts[parts.length - 1] || input
|
||||
}
|
||||
|
||||
export function DialogEditProject(props: { project: LocalProject }) {
|
||||
const dialog = useDialog()
|
||||
const globalSDK = useGlobalSDK()
|
||||
|
||||
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||
const defaultName = createMemo(() => props.project.name || folderName())
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.url || "",
|
||||
saving: false,
|
||||
})
|
||||
|
||||
const [dragOver, setDragOver] = createSignal(false)
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
function handleDragLeave() {
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
function handleInputChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (file) handleFileSelect(file)
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
setStore("iconUrl", "")
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
if (!props.project.id) return
|
||||
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
name,
|
||||
icon: { color: store.color, url: store.iconUrl },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Edit project">
|
||||
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="flex flex-col gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder={folderName()}
|
||||
value={store.name}
|
||||
onChange={(v) => setStore("name", v)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Icon</label>
|
||||
<div class="flex gap-3 items-start">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||
"border-border-base hover:border-border-strong": !dragOver(),
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => document.getElementById("icon-upload")?.click()}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
{...getAvatarColors(store.color)}
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={store.iconUrl}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
|
||||
onClick={clearIcon}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-icon-base" />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
|
||||
<span>Click or drag an image</span>
|
||||
<span>Recommended: 128x128px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">Color</label>
|
||||
<div class="flex gap-2">
|
||||
<For each={AVATAR_COLOR_KEYS}>
|
||||
{(color) => (
|
||||
<button
|
||||
type="button"
|
||||
class="relative size-8 rounded-md transition-all"
|
||||
classList={{
|
||||
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
|
||||
store.color === color,
|
||||
}}
|
||||
style={{ background: getAvatarColors(color).background }}
|
||||
onClick={() => setStore("color", color)}
|
||||
>
|
||||
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||
{store.saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -248,6 +248,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const handlePaste = async (event: ClipboardEvent) => {
|
||||
if (!isFocused()) return
|
||||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) return
|
||||
|
||||
@@ -270,7 +271,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
const handleGlobalDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
const hasFiles = event.dataTransfer?.types.includes("Files")
|
||||
if (hasFiles) {
|
||||
@@ -278,15 +279,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
const related = event.relatedTarget as Node | null
|
||||
const form = event.currentTarget as HTMLElement
|
||||
if (!related || !form.contains(related)) {
|
||||
const handleGlobalDragLeave = (event: DragEvent) => {
|
||||
// relatedTarget is null when leaving the document window
|
||||
if (!event.relatedTarget) {
|
||||
setStore("dragging", false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = async (event: DragEvent) => {
|
||||
const handleGlobalDrop = async (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
setStore("dragging", false)
|
||||
|
||||
@@ -302,9 +302,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
onMount(() => {
|
||||
editorRef.addEventListener("paste", handlePaste)
|
||||
document.addEventListener("dragover", handleGlobalDragOver)
|
||||
document.addEventListener("dragleave", handleGlobalDragLeave)
|
||||
document.addEventListener("drop", handleGlobalDrop)
|
||||
})
|
||||
onCleanup(() => {
|
||||
editorRef.removeEventListener("paste", handlePaste)
|
||||
document.removeEventListener("dragover", handleGlobalDragOver)
|
||||
document.removeEventListener("dragleave", handleGlobalDragLeave)
|
||||
document.removeEventListener("drop", handleGlobalDrop)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -1024,9 +1030,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Show>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
classList={{
|
||||
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
|
||||
"rounded-md overflow-clip focus-within:shadow-xs-border": true,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
export function SessionContextUsage() {
|
||||
const sync = useSync()
|
||||
@@ -35,19 +35,13 @@ export function SessionContextUsage() {
|
||||
{(ctx) => (
|
||||
<Tooltip
|
||||
value={
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Tokens</span>
|
||||
<span class="text-left flex-1">{ctx().tokens}</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Usage</span>
|
||||
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="opacity-70 text-right flex-1">Cost</span>
|
||||
<span class="text-left flex-1">{cost()}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||
<span class="opacity-70 text-right">Tokens</span>
|
||||
<span class="text-left">{ctx().tokens}</span>
|
||||
<span class="opacity-70 text-right">Usage</span>
|
||||
<span class="text-left">{ctx().percentage ?? 0}%</span>
|
||||
<span class="opacity-70 text-right">Cost</span>
|
||||
<span class="text-left">{cost()}</span>
|
||||
</div>
|
||||
}
|
||||
placement="top"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/terminal"
|
||||
import { usePrefersDark } from "@solid-primitives/media"
|
||||
import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
|
||||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
||||
onConnectError?: (error: unknown) => void
|
||||
}
|
||||
|
||||
type TerminalColors = {
|
||||
background: string
|
||||
foreground: string
|
||||
cursor: string
|
||||
}
|
||||
|
||||
const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
|
||||
light: {
|
||||
background: "#fcfcfc",
|
||||
foreground: "#211e1e",
|
||||
cursor: "#211e1e",
|
||||
},
|
||||
dark: {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#d4d4d4",
|
||||
},
|
||||
}
|
||||
|
||||
export const Terminal = (props: TerminalProps) => {
|
||||
const sdk = useSDK()
|
||||
const theme = useTheme()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||
let ws: WebSocket
|
||||
@@ -22,7 +42,64 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
const prefersDark = usePrefersDark()
|
||||
|
||||
const getTerminalColors = (): TerminalColors => {
|
||||
const mode = theme.mode()
|
||||
const fallback = DEFAULT_TERMINAL_COLORS[mode]
|
||||
const currentTheme = theme.themes()[theme.themeId()]
|
||||
if (!currentTheme) return fallback
|
||||
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
|
||||
if (!variant?.seeds) return fallback
|
||||
const resolved = resolveThemeVariant(variant, mode === "dark")
|
||||
const text = resolved["text-base"] ?? fallback.foreground
|
||||
const background = resolved["background-stronger"] ?? fallback.background
|
||||
return {
|
||||
background,
|
||||
foreground: text,
|
||||
cursor: text,
|
||||
}
|
||||
}
|
||||
|
||||
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
|
||||
|
||||
createEffect(() => {
|
||||
const colors = getTerminalColors()
|
||||
setTerminalColors(colors)
|
||||
if (!term) return
|
||||
const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
|
||||
if (!setOption) return
|
||||
setOption("theme", colors)
|
||||
})
|
||||
|
||||
const focusTerminal = () => term?.focus()
|
||||
const copySelection = () => {
|
||||
if (!term || !term.hasSelection()) return false
|
||||
const selection = term.getSelection()
|
||||
if (!selection) return false
|
||||
const clipboard = navigator.clipboard
|
||||
if (clipboard?.writeText) {
|
||||
clipboard.writeText(selection).catch(() => {})
|
||||
return true
|
||||
}
|
||||
if (!document.body) return false
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = selection
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
return copied
|
||||
}
|
||||
const handlePointerDown = () => {
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement instanceof HTMLElement && activeElement !== container) {
|
||||
activeElement.blur()
|
||||
}
|
||||
focusTerminal()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
ghostty = await Ghostty.load()
|
||||
@@ -33,23 +110,22 @@ export const Terminal = (props: TerminalProps) => {
|
||||
fontSize: 14,
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
allowTransparency: true,
|
||||
theme: prefersDark()
|
||||
? {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#d4d4d4",
|
||||
}
|
||||
: {
|
||||
background: "#fcfcfc",
|
||||
foreground: "#211e1e",
|
||||
cursor: "#211e1e",
|
||||
},
|
||||
theme: terminalColors(),
|
||||
scrollback: 10_000,
|
||||
ghostty,
|
||||
})
|
||||
term.attachCustomKeyEventHandler((event) => {
|
||||
const key = event.key.toLowerCase()
|
||||
if (key === "c") {
|
||||
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
|
||||
const linuxCopy = event.ctrlKey && event.shiftKey && !event.metaKey
|
||||
if ((macCopy || linuxCopy) && copySelection()) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
}
|
||||
// allow for ctrl-` to toggle terminal in parent
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "`") {
|
||||
if (event.ctrlKey && key === "`") {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
@@ -62,6 +138,8 @@ export const Terminal = (props: TerminalProps) => {
|
||||
term.loadAddon(fitAddon)
|
||||
|
||||
term.open(container)
|
||||
container.addEventListener("pointerdown", handlePointerDown)
|
||||
focusTerminal()
|
||||
|
||||
if (local.pty.buffer) {
|
||||
if (local.pty.rows && local.pty.cols) {
|
||||
@@ -75,8 +153,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
fitAddon.fit()
|
||||
}
|
||||
|
||||
container.focus()
|
||||
|
||||
fitAddon.observeResize()
|
||||
handleResize = () => fitAddon.fit()
|
||||
window.addEventListener("resize", handleResize)
|
||||
@@ -134,6 +210,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
if (handleResize) {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
container.removeEventListener("pointerdown", handlePointerDown)
|
||||
if (serializeAddon && props.onCleanup) {
|
||||
const buffer = serializeAddon.serialize()
|
||||
props.onCleanup({
|
||||
@@ -153,6 +230,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
ref={container}
|
||||
data-component="terminal"
|
||||
data-prevent-autofocus
|
||||
style={{ "background-color": terminalColors().background }}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
"size-full px-6 py-3 font-mono": true,
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface CommandOption {
|
||||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
export function parseKeybind(config: string): Keybind[] {
|
||||
@@ -115,6 +116,28 @@ export function formatKeybind(config: string): string {
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
let cleanup: (() => void) | void
|
||||
let committed = false
|
||||
|
||||
const handleMove = (option: CommandOption | undefined) => {
|
||||
cleanup?.()
|
||||
cleanup = option?.onHighlight?.()
|
||||
}
|
||||
|
||||
const handleSelect = (option: CommandOption | undefined) => {
|
||||
if (option) {
|
||||
committed = true
|
||||
cleanup = undefined
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (!committed) {
|
||||
cleanup?.()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
@@ -125,12 +148,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}}
|
||||
onMove={handleMove}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { ErrorPage, type InitError } from "../pages/error"
|
||||
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
@@ -71,22 +71,19 @@ function createGlobalSync() {
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
children: Record<string, State>
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
children: {},
|
||||
})
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
const permissionListeners: Set<(info: { directory: string; permission: Permission }) => void> = new Set()
|
||||
function child(directory: string) {
|
||||
if (!directory) console.error("No directory provided")
|
||||
if (!children[directory]) {
|
||||
setGlobalStore("children", directory, {
|
||||
children[directory] = createStore<State>({
|
||||
project: "",
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
@@ -106,7 +103,6 @@ function createGlobalSync() {
|
||||
message: {},
|
||||
part: {},
|
||||
})
|
||||
children[directory] = createStore(globalStore.children[directory])
|
||||
bootstrapInstance(directory)
|
||||
}
|
||||
return children[directory]
|
||||
@@ -128,7 +124,7 @@ function createGlobalSync() {
|
||||
const updated = new Date(s.time.updated).getTime()
|
||||
return updated > fourHoursAgo
|
||||
})
|
||||
setStore("session", sessions)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load sessions", err)
|
||||
@@ -139,7 +135,7 @@ function createGlobalSync() {
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
if (!directory) return
|
||||
const [, setStore] = child(directory)
|
||||
const [store, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
@@ -171,12 +167,32 @@ function createGlobalSync() {
|
||||
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
permission: () =>
|
||||
sdk.permission.list().then((x) => {
|
||||
const grouped: Record<string, typeof x.data> = {}
|
||||
const grouped: Record<string, Permission[]> = {}
|
||||
for (const perm of x.data ?? []) {
|
||||
grouped[perm.sessionID] = grouped[perm.sessionID] ?? []
|
||||
grouped[perm.sessionID]!.push(perm)
|
||||
const existing = grouped[perm.sessionID]
|
||||
if (existing) {
|
||||
existing.push(perm)
|
||||
continue
|
||||
}
|
||||
grouped[perm.sessionID] = [perm]
|
||||
}
|
||||
setStore("permission", grouped)
|
||||
|
||||
batch(() => {
|
||||
for (const sessionID of Object.keys(store.permission)) {
|
||||
if (grouped[sessionID]) continue
|
||||
setStore("permission", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
reconcile(
|
||||
permissions.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||
@@ -247,7 +263,7 @@ function createGlobalSync() {
|
||||
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
||||
break
|
||||
case "todo.updated":
|
||||
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
|
||||
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
|
||||
break
|
||||
case "session.status": {
|
||||
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
|
||||
@@ -329,29 +345,26 @@ function createGlobalSync() {
|
||||
break
|
||||
}
|
||||
case "permission.updated": {
|
||||
const permissions = store.permission[event.properties.sessionID]
|
||||
const isNew = !permissions || !permissions.find((p) => p.id === event.properties.id)
|
||||
const sessionID = event.properties.sessionID
|
||||
const permissions = store.permission[sessionID]
|
||||
if (!permissions) {
|
||||
setStore("permission", event.properties.sessionID, [event.properties])
|
||||
} else {
|
||||
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
setStore(
|
||||
"permission",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
if (result.found) {
|
||||
draft[result.index] = event.properties
|
||||
return
|
||||
}
|
||||
draft.push(event.properties)
|
||||
}),
|
||||
)
|
||||
setStore("permission", sessionID, [event.properties])
|
||||
break
|
||||
}
|
||||
if (isNew) {
|
||||
for (const listener of permissionListeners) {
|
||||
listener({ directory, permission: event.properties })
|
||||
}
|
||||
|
||||
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore("permission", sessionID, result.index, reconcile(event.properties))
|
||||
break
|
||||
}
|
||||
|
||||
setStore(
|
||||
"permission",
|
||||
sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, event.properties)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "permission.replied": {
|
||||
@@ -368,6 +381,15 @@ function createGlobalSync() {
|
||||
)
|
||||
break
|
||||
}
|
||||
case "lsp.updated": {
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -439,12 +461,6 @@ function createGlobalSync() {
|
||||
project: {
|
||||
loadSessions,
|
||||
},
|
||||
permission: {
|
||||
onUpdated(listener: (info: { directory: string; permission: Permission }) => void) {
|
||||
permissionListeners.add(listener)
|
||||
return () => permissionListeners.delete(listener)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
{
|
||||
...project,
|
||||
...(metadata ?? {}),
|
||||
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { createEffect, createRoot, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { Permission } from "@opencode-ai/sdk/v2/client"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
type PermissionsBySession = {
|
||||
[sessionID: string]: Permission[]
|
||||
}
|
||||
|
||||
type PermissionRespondFn = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => void
|
||||
|
||||
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
|
||||
|
||||
function shouldAutoAccept(perm: Permission) {
|
||||
return AUTO_ACCEPT_TYPES.has(perm.type)
|
||||
}
|
||||
|
||||
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
|
||||
name: "Permission",
|
||||
init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
"permission.v1",
|
||||
createStore({
|
||||
autoAcceptEdits: {} as Record<string, boolean>,
|
||||
}),
|
||||
)
|
||||
|
||||
const responded = new Set<string>()
|
||||
const watches = new Map<string, () => void>()
|
||||
|
||||
function respond(perm: Permission) {
|
||||
if (responded.has(perm.id)) return
|
||||
responded.add(perm.id)
|
||||
props.onRespond({
|
||||
sessionID: perm.sessionID,
|
||||
permissionID: perm.id,
|
||||
response: "once",
|
||||
})
|
||||
}
|
||||
|
||||
function watch(sessionID: string) {
|
||||
if (watches.has(sessionID)) return
|
||||
|
||||
const dispose = createRoot((dispose) => {
|
||||
createEffect(() => {
|
||||
if (!store.autoAcceptEdits[sessionID]) return
|
||||
|
||||
const permissions = props.permissions[sessionID] ?? []
|
||||
permissions.length
|
||||
|
||||
for (const perm of permissions) {
|
||||
if (!shouldAutoAccept(perm)) continue
|
||||
respond(perm)
|
||||
}
|
||||
})
|
||||
|
||||
return dispose
|
||||
})
|
||||
|
||||
watches.set(sessionID, dispose)
|
||||
}
|
||||
|
||||
function unwatch(sessionID: string) {
|
||||
const dispose = watches.get(sessionID)
|
||||
if (!dispose) return
|
||||
dispose()
|
||||
watches.delete(sessionID)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
|
||||
for (const sessionID in store.autoAcceptEdits) {
|
||||
if (!store.autoAcceptEdits[sessionID]) continue
|
||||
watch(sessionID)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
for (const dispose of watches.values()) dispose()
|
||||
watches.clear()
|
||||
})
|
||||
|
||||
function enable(sessionID: string) {
|
||||
setStore("autoAcceptEdits", sessionID, true)
|
||||
watch(sessionID)
|
||||
|
||||
const permissions = props.permissions[sessionID] ?? []
|
||||
for (const perm of permissions) {
|
||||
if (!shouldAutoAccept(perm)) continue
|
||||
respond(perm)
|
||||
}
|
||||
}
|
||||
|
||||
function disable(sessionID: string) {
|
||||
setStore("autoAcceptEdits", sessionID, false)
|
||||
unwatch(sessionID)
|
||||
}
|
||||
|
||||
return {
|
||||
get permissions() {
|
||||
return props.permissions
|
||||
},
|
||||
respond: props.onRespond,
|
||||
isAutoAccepting(sessionID: string) {
|
||||
return store.autoAcceptEdits[sessionID] ?? false
|
||||
},
|
||||
toggleAutoAccept(sessionID: string) {
|
||||
if (store.autoAcceptEdits[sessionID]) {
|
||||
disable(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
enable(sessionID)
|
||||
},
|
||||
enableAutoAccept(sessionID: string) {
|
||||
if (store.autoAcceptEdits[sessionID]) return
|
||||
enable(sessionID)
|
||||
},
|
||||
disableAutoAccept(sessionID: string) {
|
||||
disable(sessionID)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { produce } from "solid-js/store"
|
||||
import { createMemo } from "solid-js"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { produce, reconcile } from "solid-js/store"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { retry } from "@opencode-ai/util/retry"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
@@ -56,7 +56,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, message)
|
||||
}
|
||||
draft.part[input.messageID] = input.parts.slice()
|
||||
draft.part[input.messageID] = input.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
}),
|
||||
)
|
||||
},
|
||||
@@ -67,22 +67,46 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
retry(() => sdk.client.session.todo({ sessionID })),
|
||||
retry(() => sdk.client.session.diff({ sessionID })),
|
||||
])
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session[match.index] = session.data!
|
||||
if (!match.found) draft.session.splice(match.index, 0, session.data!)
|
||||
draft.todo[sessionID] = todo.data ?? []
|
||||
draft.message[sessionID] = messages
|
||||
.data!.map((x) => x.info)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
for (const message of messages.data!) {
|
||||
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
draft.session_diff[sessionID] = diff.data ?? []
|
||||
}),
|
||||
)
|
||||
|
||||
batch(() => {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||
if (match.found) {
|
||||
draft[match.index] = session.data!
|
||||
return
|
||||
}
|
||||
draft.splice(match.index, 0, session.data!)
|
||||
}),
|
||||
)
|
||||
|
||||
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||
setStore(
|
||||
"message",
|
||||
sessionID,
|
||||
reconcile(
|
||||
(messages.data ?? [])
|
||||
.map((x) => x.info)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
|
||||
for (const message of messages.data ?? []) {
|
||||
setStore(
|
||||
"part",
|
||||
message.info.id,
|
||||
reconcile(
|
||||
message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||
{ key: "id" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||
})
|
||||
},
|
||||
fetch: async (count = 10) => {
|
||||
setStore("limit", (x) => x + count)
|
||||
@@ -91,7 +115,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", sessions)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
})
|
||||
},
|
||||
more: createMemo(() => store.session.length >= store.limit),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from "@solidjs/router"
|
||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||
import { SyncProvider, useSync } from "@/context/sync"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { PermissionProvider } from "@/context/permission"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
@@ -19,16 +20,18 @@ export default function Layout(props: ParentProps) {
|
||||
{iife(() => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const respond = (input: {
|
||||
sessionID: string
|
||||
permissionID: string
|
||||
response: "once" | "always" | "reject"
|
||||
}) => sdk.client.permission.respond(input)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
data={sync.data}
|
||||
directory={directory()}
|
||||
onPermissionRespond={(input) => {
|
||||
sdk.client.permission.respond(input)
|
||||
}}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
<PermissionProvider permissions={sync.data.permission} onRespond={respond}>
|
||||
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
</PermissionProvider>
|
||||
)
|
||||
})}
|
||||
</SyncProvider>
|
||||
|
||||
@@ -41,14 +41,16 @@ import {
|
||||
} from "@thisbeyond/solid-dnd"
|
||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { showToast, Toast } from "@opencode-ai/ui/toast"
|
||||
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { Header } from "@/components/header"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
@@ -89,6 +91,41 @@ export default function Layout(props: ParentProps) {
|
||||
const providers = useProviders()
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const theme = useTheme()
|
||||
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||
const colorSchemeLabel: Record<ColorScheme, string> = {
|
||||
system: "System",
|
||||
light: "Light",
|
||||
dark: "Dark",
|
||||
}
|
||||
|
||||
function cycleTheme(direction = 1) {
|
||||
const ids = availableThemeEntries().map(([id]) => id)
|
||||
if (ids.length === 0) return
|
||||
const currentIndex = ids.indexOf(theme.themeId())
|
||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
|
||||
const nextThemeId = ids[nextIndex]
|
||||
theme.setTheme(nextThemeId)
|
||||
const nextTheme = theme.themes()[nextThemeId]
|
||||
showToast({
|
||||
title: "Theme switched",
|
||||
description: nextTheme?.name ?? nextThemeId,
|
||||
})
|
||||
}
|
||||
|
||||
function cycleColorScheme(direction = 1) {
|
||||
const current = theme.colorScheme()
|
||||
const currentIndex = colorSchemeOrder.indexOf(current)
|
||||
const nextIndex =
|
||||
currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
|
||||
const next = colorSchemeOrder[nextIndex]
|
||||
theme.setColorScheme(next)
|
||||
showToast({
|
||||
title: "Color scheme",
|
||||
description: colorSchemeLabel[next],
|
||||
})
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (platform.checkUpdate && platform.update && platform.restart) {
|
||||
@@ -118,7 +155,15 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const unsub = globalSync.permission.onUpdated(({ directory, permission }) => {
|
||||
const seenSessions = new Set<string>()
|
||||
const toastBySession = new Map<string, number>()
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
if (e.details?.type !== "permission.updated") return
|
||||
const directory = e.name
|
||||
const permission = e.details.properties
|
||||
const sessionKey = `${directory}:${permission.sessionID}`
|
||||
if (seenSessions.has(sessionKey)) return
|
||||
seenSessions.add(sessionKey)
|
||||
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||
const currentSession = params.id
|
||||
if (directory === currentDir && permission.sessionID === currentSession) return
|
||||
@@ -127,7 +172,7 @@ export default function Layout(props: ParentProps) {
|
||||
if (directory === currentDir && session?.parentID === currentSession) return
|
||||
const sessionTitle = session?.title ?? "New session"
|
||||
const projectName = getFilename(directory)
|
||||
showToast({
|
||||
const toastId = showToast({
|
||||
persistent: true,
|
||||
icon: "checklist",
|
||||
title: "Permission required",
|
||||
@@ -138,7 +183,6 @@ export default function Layout(props: ParentProps) {
|
||||
onClick: () => {
|
||||
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
|
||||
},
|
||||
dismissAfter: true,
|
||||
},
|
||||
{
|
||||
label: "Dismiss",
|
||||
@@ -146,8 +190,33 @@ export default function Layout(props: ParentProps) {
|
||||
},
|
||||
],
|
||||
})
|
||||
toastBySession.set(sessionKey, toastId)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
createEffect(() => {
|
||||
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||
const currentSession = params.id
|
||||
if (!currentDir || !currentSession) return
|
||||
const sessionKey = `${currentDir}:${currentSession}`
|
||||
const toastId = toastBySession.get(sessionKey)
|
||||
if (toastId !== undefined) {
|
||||
toaster.dismiss(toastId)
|
||||
toastBySession.delete(sessionKey)
|
||||
seenSessions.delete(sessionKey)
|
||||
}
|
||||
const [store] = globalSync.child(currentDir)
|
||||
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||
for (const child of childSessions) {
|
||||
const childKey = `${currentDir}:${child.id}`
|
||||
const childToastId = toastBySession.get(childKey)
|
||||
if (childToastId !== undefined) {
|
||||
toaster.dismiss(childToastId)
|
||||
toastBySession.delete(childKey)
|
||||
seenSessions.delete(childKey)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function sortSessions(a: Session, b: Session) {
|
||||
@@ -254,57 +323,102 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
}
|
||||
|
||||
command.register(() => [
|
||||
{
|
||||
id: "sidebar.toggle",
|
||||
title: "Toggle sidebar",
|
||||
category: "View",
|
||||
keybind: "mod+b",
|
||||
onSelect: () => layout.sidebar.toggle(),
|
||||
},
|
||||
...(platform.openDirectoryPickerDialog
|
||||
? [
|
||||
{
|
||||
id: "project.open",
|
||||
title: "Open project",
|
||||
category: "Project",
|
||||
keybind: "mod+o",
|
||||
onSelect: () => chooseProject(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "provider.connect",
|
||||
title: "Connect provider",
|
||||
category: "Provider",
|
||||
onSelect: () => connectProvider(),
|
||||
},
|
||||
{
|
||||
id: "session.previous",
|
||||
title: "Previous session",
|
||||
category: "Session",
|
||||
keybind: "alt+arrowup",
|
||||
onSelect: () => navigateSessionByOffset(-1),
|
||||
},
|
||||
{
|
||||
id: "session.next",
|
||||
title: "Next session",
|
||||
category: "Session",
|
||||
keybind: "alt+arrowdown",
|
||||
onSelect: () => navigateSessionByOffset(1),
|
||||
},
|
||||
{
|
||||
id: "session.archive",
|
||||
title: "Archive session",
|
||||
category: "Session",
|
||||
keybind: "mod+shift+backspace",
|
||||
disabled: !params.dir || !params.id,
|
||||
onSelect: () => {
|
||||
const session = currentSessions().find((s) => s.id === params.id)
|
||||
if (session) archiveSession(session)
|
||||
command.register(() => {
|
||||
const commands: CommandOption[] = [
|
||||
{
|
||||
id: "sidebar.toggle",
|
||||
title: "Toggle sidebar",
|
||||
category: "View",
|
||||
keybind: "mod+b",
|
||||
onSelect: () => layout.sidebar.toggle(),
|
||||
},
|
||||
},
|
||||
])
|
||||
...(platform.openDirectoryPickerDialog
|
||||
? [
|
||||
{
|
||||
id: "project.open",
|
||||
title: "Open project",
|
||||
category: "Project",
|
||||
keybind: "mod+o",
|
||||
onSelect: () => chooseProject(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "provider.connect",
|
||||
title: "Connect provider",
|
||||
category: "Provider",
|
||||
onSelect: () => connectProvider(),
|
||||
},
|
||||
{
|
||||
id: "session.previous",
|
||||
title: "Previous session",
|
||||
category: "Session",
|
||||
keybind: "alt+arrowup",
|
||||
onSelect: () => navigateSessionByOffset(-1),
|
||||
},
|
||||
{
|
||||
id: "session.next",
|
||||
title: "Next session",
|
||||
category: "Session",
|
||||
keybind: "alt+arrowdown",
|
||||
onSelect: () => navigateSessionByOffset(1),
|
||||
},
|
||||
{
|
||||
id: "session.archive",
|
||||
title: "Archive session",
|
||||
category: "Session",
|
||||
keybind: "mod+shift+backspace",
|
||||
disabled: !params.dir || !params.id,
|
||||
onSelect: () => {
|
||||
const session = currentSessions().find((s) => s.id === params.id)
|
||||
if (session) archiveSession(session)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "theme.cycle",
|
||||
title: "Cycle theme",
|
||||
category: "Theme",
|
||||
keybind: "mod+shift+t",
|
||||
onSelect: () => cycleTheme(1),
|
||||
},
|
||||
]
|
||||
|
||||
for (const [id, definition] of availableThemeEntries()) {
|
||||
commands.push({
|
||||
id: `theme.set.${id}`,
|
||||
title: `Use theme: ${definition.name ?? id}`,
|
||||
category: "Theme",
|
||||
onSelect: () => theme.commitPreview(),
|
||||
onHighlight: () => {
|
||||
theme.previewTheme(id)
|
||||
return () => theme.cancelPreview()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
commands.push({
|
||||
id: "theme.scheme.cycle",
|
||||
title: "Cycle color scheme",
|
||||
category: "Theme",
|
||||
keybind: "mod+shift+s",
|
||||
onSelect: () => cycleColorScheme(1),
|
||||
})
|
||||
|
||||
for (const scheme of colorSchemeOrder) {
|
||||
commands.push({
|
||||
id: `theme.scheme.${scheme}`,
|
||||
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
|
||||
category: "Theme",
|
||||
onSelect: () => theme.commitPreview(),
|
||||
onHighlight: () => {
|
||||
theme.previewColorScheme(scheme)
|
||||
return () => theme.cancelPreview()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return commands
|
||||
})
|
||||
|
||||
function connectProvider() {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
@@ -409,7 +523,7 @@ export default function Layout(props: ParentProps) {
|
||||
const notification = useNotification()
|
||||
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
@@ -445,7 +559,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const current = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
return (
|
||||
<Switch>
|
||||
@@ -588,7 +702,7 @@ export default function Layout(props: ParentProps) {
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||
@@ -634,6 +748,11 @@ export default function Layout(props: ParentProps) {
|
||||
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
|
||||
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -60,6 +60,14 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { StatusBar } from "@/components/status-bar"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -74,22 +82,40 @@ export default function Page() {
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const permission = usePermission()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() =>
|
||||
messages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
const userMessages = createMemo(
|
||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const visibleUserMessages = createMemo(
|
||||
() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => lastUserMessage()?.id,
|
||||
() => {
|
||||
const msg = lastUserMessage()
|
||||
if (!msg) return
|
||||
if (msg.agent) local.agent.set(msg.agent)
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
},
|
||||
),
|
||||
)
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
@@ -159,16 +185,37 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
params.id
|
||||
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", status.type !== "idle")
|
||||
})
|
||||
})
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => params.id,
|
||||
(id) => {
|
||||
const status = sync.data.session_status[id ?? ""] ?? idle
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", status.type !== "idle")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => status().type,
|
||||
(type) => {
|
||||
if (type !== "idle") return
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", false)
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
|
||||
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
|
||||
|
||||
createRenderEffect((prev) => {
|
||||
@@ -304,6 +351,22 @@ export default function Page() {
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => local.agent.move(-1),
|
||||
},
|
||||
{
|
||||
id: "permissions.autoaccept",
|
||||
title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
|
||||
category: "Permissions",
|
||||
disabled: !params.id,
|
||||
onSelect: () => {
|
||||
if (!params.id) return
|
||||
permission.toggleAutoAccept(params.id)
|
||||
showToast({
|
||||
title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
|
||||
description: permission.isAutoAccepting(params.id)
|
||||
? "Edit and write permissions will be automatically approved"
|
||||
: "Edit and write permissions will require approval",
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.undo",
|
||||
title: "Undo",
|
||||
@@ -575,6 +638,7 @@ export default function Page() {
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
@@ -631,6 +695,7 @@ export default function Page() {
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={activeMessage()!.id}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
stepsExpanded={store.stepsExpanded}
|
||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createSignal, For, Show } from "solid-js"
|
||||
import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
|
||||
interface TaskSource {
|
||||
repo: string
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
interface Judge {
|
||||
score: number
|
||||
rationale: string
|
||||
judge: string
|
||||
}
|
||||
|
||||
interface ScoreDetail {
|
||||
criterion: string
|
||||
weight: number
|
||||
average: number
|
||||
variance?: number
|
||||
judges?: Judge[]
|
||||
}
|
||||
|
||||
interface RunUsage {
|
||||
input: number
|
||||
output: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface Run {
|
||||
task: string
|
||||
model: string
|
||||
agent: string
|
||||
score: {
|
||||
final: number
|
||||
base: number
|
||||
penalty: number
|
||||
}
|
||||
scoreDetails: ScoreDetail[]
|
||||
usage?: RunUsage
|
||||
duration?: number
|
||||
}
|
||||
|
||||
interface Prompt {
|
||||
commit: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
interface AverageUsage {
|
||||
input: number
|
||||
output: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface Task {
|
||||
averageScore: number
|
||||
averageDuration?: number
|
||||
averageUsage?: AverageUsage
|
||||
model?: string
|
||||
agent?: string
|
||||
summary?: string
|
||||
runs?: Run[]
|
||||
task: {
|
||||
id: string
|
||||
source: TaskSource
|
||||
prompts?: Prompt[]
|
||||
}
|
||||
}
|
||||
|
||||
interface BenchmarkResult {
|
||||
averageScore: number
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
async function getTaskDetail(benchmarkId: string, taskId: string) {
|
||||
"use server"
|
||||
const rows = await Database.use((tx) =>
|
||||
tx.select().from(BenchmarkTable).where(eq(BenchmarkTable.id, benchmarkId)).limit(1),
|
||||
)
|
||||
if (!rows[0]) return null
|
||||
const parsed = JSON.parse(rows[0].result) as BenchmarkResult
|
||||
const task = parsed.tasks.find((t) => t.task.id === taskId)
|
||||
return task ?? null
|
||||
}
|
||||
|
||||
const queryTaskDetail = query(getTaskDetail, "benchmark.task.detail")
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`
|
||||
}
|
||||
return `${remainingSeconds}s`
|
||||
}
|
||||
|
||||
export default function BenchDetail() {
|
||||
const params = useParams()
|
||||
const [benchmarkId, taskId] = (params.id ?? "").split(":")
|
||||
const task = createAsync(() => queryTaskDetail(benchmarkId, taskId))
|
||||
|
||||
return (
|
||||
<main data-page="bench-detail">
|
||||
<Title>Benchmark - {taskId}</Title>
|
||||
<div style={{ padding: "1rem" }}>
|
||||
<Show when={task()} fallback={<p>Task not found</p>}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Agent: </strong>
|
||||
{task()?.agent ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Model: </strong>
|
||||
{task()?.model ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Task: </strong>
|
||||
{task()!.task.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Repo: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.repo}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>From: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.from}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.from.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>To: </strong>
|
||||
<a
|
||||
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.to}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "#0066cc" }}
|
||||
>
|
||||
{task()!.task.source.to.slice(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={task()?.task.prompts && task()!.task.prompts!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Prompt:</strong>
|
||||
<For each={task()!.task.prompts}>
|
||||
{(p) => (
|
||||
<div style={{ "margin-top": "0.5rem" }}>
|
||||
<div style={{ "font-size": "0.875rem", color: "#666" }}>Commit: {p.commit.slice(0, 7)}</div>
|
||||
<p style={{ "margin-top": "0.25rem", "white-space": "pre-wrap" }}>{p.prompt}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<hr style={{ margin: "1rem 0", border: "none", "border-top": "1px solid #ccc" }} />
|
||||
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<div>
|
||||
<strong>Average Duration: </strong>
|
||||
{task()?.averageDuration ? formatDuration(task()!.averageDuration!) : "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Average Score: </strong>
|
||||
{task()?.averageScore?.toFixed(3) ?? "N/A"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Average Cost: </strong>
|
||||
{task()?.averageUsage?.cost ? `$${task()!.averageUsage!.cost.toFixed(4)}` : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={task()?.summary}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Summary:</strong>
|
||||
<p style={{ "margin-top": "0.5rem", "white-space": "pre-wrap" }}>{task()!.summary}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={task()?.runs && task()!.runs!.length > 0}>
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<strong>Runs:</strong>
|
||||
<table style={{ "margin-top": "0.5rem", "border-collapse": "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Run</th>
|
||||
<th
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
padding: "0.5rem",
|
||||
"text-align": "left",
|
||||
"white-space": "nowrap",
|
||||
}}
|
||||
>
|
||||
Score (Base - Penalty)
|
||||
</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Cost</th>
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Duration</th>
|
||||
<For each={task()!.runs![0]?.scoreDetails}>
|
||||
{(detail) => (
|
||||
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>
|
||||
{detail.criterion} ({detail.weight})
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={task()!.runs}>
|
||||
{(run, index) => (
|
||||
<tr>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{index() + 1}</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem", "white-space": "nowrap" }}>
|
||||
{run.score.final.toFixed(3)} ({run.score.base.toFixed(3)} - {run.score.penalty.toFixed(3)})
|
||||
</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{run.usage?.cost ? `$${run.usage.cost.toFixed(4)}` : "N/A"}
|
||||
</td>
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
{run.duration ? formatDuration(run.duration) : "N/A"}
|
||||
</td>
|
||||
<For each={run.scoreDetails}>
|
||||
{(detail) => (
|
||||
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||
<For each={detail.judges}>
|
||||
{(judge) => (
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
"margin-right": "0.25rem",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
<For each={task()!.runs}>
|
||||
{(run, index) => (
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<h3 style={{ margin: "0 0 0.5rem 0" }}>Run {index() + 1}</h3>
|
||||
<div>
|
||||
<strong>Score: </strong>
|
||||
{run.score.final.toFixed(3)} (Base: {run.score.base.toFixed(3)} - Penalty:{" "}
|
||||
{run.score.penalty.toFixed(3)})
|
||||
</div>
|
||||
<For each={run.scoreDetails}>
|
||||
{(detail) => (
|
||||
<div style={{ "margin-top": "1rem", "padding-left": "1rem", "border-left": "2px solid #ccc" }}>
|
||||
<div>
|
||||
{detail.criterion} (weight: {detail.weight}){" "}
|
||||
<For each={detail.judges}>
|
||||
{(judge) => (
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
"margin-right": "0.25rem",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={detail.judges && detail.judges.length > 0}>
|
||||
<For each={detail.judges}>
|
||||
{(judge) => {
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
return (
|
||||
<div style={{ "margin-top": "0.5rem", "padding-left": "1rem" }}>
|
||||
<div
|
||||
style={{ "font-size": "0.875rem", cursor: "pointer" }}
|
||||
onClick={() => setExpanded(!expanded())}
|
||||
>
|
||||
<span style={{ "margin-right": "0.5rem" }}>{expanded() ? "▼" : "▶"}</span>
|
||||
<span
|
||||
style={{
|
||||
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||
}}
|
||||
>
|
||||
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||
</span>{" "}
|
||||
{judge.judge}
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<p
|
||||
style={{
|
||||
margin: "0.25rem 0 0 0",
|
||||
"white-space": "pre-wrap",
|
||||
"font-size": "0.875rem",
|
||||
}}
|
||||
>
|
||||
{judge.rationale}
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{(() => {
|
||||
const [jsonExpanded, setJsonExpanded] = createSignal(false)
|
||||
return (
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<button
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "0.75rem 1.5rem",
|
||||
"font-size": "1rem",
|
||||
background: "#f0f0f0",
|
||||
border: "1px solid #ccc",
|
||||
"border-radius": "4px",
|
||||
}}
|
||||
onClick={() => setJsonExpanded(!jsonExpanded())}
|
||||
>
|
||||
<span style={{ "margin-right": "0.5rem" }}>{jsonExpanded() ? "▼" : "▶"}</span>
|
||||
Raw JSON
|
||||
</button>
|
||||
<Show when={jsonExpanded()}>
|
||||
<pre>{JSON.stringify(task(), null, 2)}</pre>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { A, createAsync, query } from "@solidjs/router"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { Database, desc } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
|
||||
interface BenchmarkResult {
|
||||
averageScore: number
|
||||
tasks: { averageScore: number; task: { id: string } }[]
|
||||
}
|
||||
|
||||
async function getBenchmarks() {
|
||||
"use server"
|
||||
const rows = await Database.use((tx) =>
|
||||
tx.select().from(BenchmarkTable).orderBy(desc(BenchmarkTable.timeCreated)).limit(100),
|
||||
)
|
||||
return rows.map((row) => {
|
||||
const parsed = JSON.parse(row.result) as BenchmarkResult
|
||||
const taskScores: Record<string, number> = {}
|
||||
for (const t of parsed.tasks) {
|
||||
taskScores[t.task.id] = t.averageScore
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
agent: row.agent,
|
||||
model: row.model,
|
||||
averageScore: parsed.averageScore,
|
||||
taskScores,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const queryBenchmarks = query(getBenchmarks, "benchmarks.list")
|
||||
|
||||
export default function Bench() {
|
||||
const benchmarks = createAsync(() => queryBenchmarks())
|
||||
|
||||
const taskIds = createMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
for (const row of benchmarks() ?? []) {
|
||||
for (const id of Object.keys(row.taskScores)) {
|
||||
ids.add(id)
|
||||
}
|
||||
}
|
||||
return [...ids].sort()
|
||||
})
|
||||
|
||||
return (
|
||||
<main data-page="bench" style={{ padding: "2rem" }}>
|
||||
<Title>Benchmark</Title>
|
||||
<h1 style={{ "margin-bottom": "1.5rem" }}>Benchmarks</h1>
|
||||
<table style={{ "border-collapse": "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Agent</th>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Model</th>
|
||||
<th style={{ "text-align": "left", padding: "0.75rem" }}>Score</th>
|
||||
<For each={taskIds()}>{(id) => <th style={{ "text-align": "left", padding: "0.75rem" }}>{id}</th>}</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={benchmarks()}>
|
||||
{(row) => (
|
||||
<tr>
|
||||
<td style={{ padding: "0.75rem" }}>{row.agent}</td>
|
||||
<td style={{ padding: "0.75rem" }}>{row.model}</td>
|
||||
<td style={{ padding: "0.75rem" }}>{row.averageScore.toFixed(3)}</td>
|
||||
<For each={taskIds()}>
|
||||
{(id) => (
|
||||
<td style={{ padding: "0.75rem" }}>
|
||||
<Show when={row.taskScores[id] !== undefined} fallback="">
|
||||
<A href={`/bench/${row.id}:${id}`} style={{ color: "#0066cc" }}>
|
||||
{row.taskScores[id]?.toFixed(3)}
|
||||
</A>
|
||||
</Show>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
|
||||
interface SubmissionBody {
|
||||
model: string
|
||||
agent: string
|
||||
result: string
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const body = (await event.request.json()) as SubmissionBody
|
||||
|
||||
if (!body.model || !body.agent || !body.result) {
|
||||
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx.insert(BenchmarkTable).values({
|
||||
id: Identifier.create("benchmark"),
|
||||
model: body.model,
|
||||
agent: body.agent,
|
||||
result: body.result,
|
||||
}),
|
||||
)
|
||||
|
||||
return Response.json({ success: true }, { status: 200 })
|
||||
}
|
||||
@@ -124,6 +124,8 @@ export async function handler(
|
||||
res.status !== 200 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
!modelInfo.stickyProvider &&
|
||||
modelInfo.fallbackProvider &&
|
||||
providerInfo.id !== modelInfo.fallbackProvider
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE `benchmark` (
|
||||
`id` varchar(30) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
`model` varchar(64) NOT NULL,
|
||||
`agent` varchar(64) NOT NULL,
|
||||
`result` mediumtext NOT NULL,
|
||||
CONSTRAINT `benchmark_id_pk` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `time_created` ON `benchmark` (`time_created`);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -274,6 +274,13 @@
|
||||
"when": 1764110043942,
|
||||
"tag": "0038_famous_magik",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "5",
|
||||
"when": 1766946179892,
|
||||
"tag": "0039_striped_forge",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,6 +5,7 @@ export namespace Identifier {
|
||||
const prefixes = {
|
||||
account: "acc",
|
||||
auth: "aut",
|
||||
benchmark: "ben",
|
||||
billing: "bil",
|
||||
key: "key",
|
||||
model: "mod",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { index, mediumtext, mysqlTable, primaryKey, varchar } from "drizzle-orm/mysql-core"
|
||||
import { id, timestamps } from "../drizzle/types"
|
||||
|
||||
export const BenchmarkTable = mysqlTable(
|
||||
"benchmark",
|
||||
{
|
||||
id: id(),
|
||||
...timestamps,
|
||||
model: varchar("model", { length: 64 }).notNull(),
|
||||
agent: varchar("agent", { length: 64 }).notNull(),
|
||||
result: mediumtext("result").notNull(),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.id] }), index("time_created").on(table.timeCreated)],
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" style="background-color: var(--background-base)">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@@ -13,14 +13,39 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script">
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
document.documentElement.setAttribute("data-theme", savedTheme)
|
||||
var themeId = localStorage.getItem("opencode-theme-id")
|
||||
if (!themeId) return
|
||||
|
||||
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
var mode = isDark ? "dark" : "light"
|
||||
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
if (themeId === "oc-1") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||
if (css) {
|
||||
var style = document.createElement("style")
|
||||
style.id = "oc-theme-preload"
|
||||
style.textContent =
|
||||
":root{color-scheme:" +
|
||||
mode +
|
||||
";--text-mix-blend-mode:" +
|
||||
(isDark ? "plus-lighter" : "multiply") +
|
||||
";" +
|
||||
css +
|
||||
"}"
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.204"
|
||||
version = "1.0.208"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.204/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -2,3 +2,5 @@ preload = ["@opentui/solid/preload"]
|
||||
|
||||
[test]
|
||||
preload = ["./test/preload.ts"]
|
||||
# Enable code coverage
|
||||
coverage = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -50,7 +50,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/anthropic": "2.0.56",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/cerebras": "1.0.33",
|
||||
"@ai-sdk/cohere": "2.0.21",
|
||||
@@ -65,7 +65,7 @@
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/perplexity": "2.0.22",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
|
||||
@@ -7,4 +7,5 @@ Rules:
|
||||
- Do not explain what the user asked for
|
||||
- Write in first person (I added..., I fixed...)
|
||||
- Never ask questions or add new questions
|
||||
- Only exception: if the conversation ends with an unanswered question to the user, preserve that exact question
|
||||
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
||||
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
|
||||
|
||||
@@ -158,6 +158,29 @@ export function parseGitHubRemote(url: string): { owner: string; repo: string }
|
||||
return { owner: match[1], repo: match[2] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts displayable text from assistant response parts.
|
||||
* Returns null for tool-only or reasoning-only responses (signals summary needed).
|
||||
* Throws for truly unusable responses (empty, step-start only, etc.).
|
||||
*/
|
||||
export function extractResponseText(parts: MessageV2.Part[]): string | null {
|
||||
// Priority 1: Look for text parts
|
||||
const textPart = parts.findLast((p) => p.type === "text")
|
||||
if (textPart) return textPart.text
|
||||
|
||||
// Priority 2: Reasoning-only - return null to signal summary needed
|
||||
const reasoningPart = parts.findLast((p) => p.type === "reasoning")
|
||||
if (reasoningPart) return null
|
||||
|
||||
// Priority 3: Tool-only - return null to signal summary needed
|
||||
const toolParts = parts.filter((p) => p.type === "tool" && p.state.status === "completed")
|
||||
if (toolParts.length > 0) return null
|
||||
|
||||
// No usable parts - throw with debug info
|
||||
const partTypes = parts.map((p) => p.type).join(", ") || "none"
|
||||
throw new Error(`Failed to parse response. Part types found: [${partTypes}]`)
|
||||
}
|
||||
|
||||
export const GithubCommand = cmd({
|
||||
command: "github",
|
||||
describe: "manage GitHub agent",
|
||||
@@ -890,10 +913,41 @@ export const GithubRunCommand = cmd({
|
||||
)
|
||||
}
|
||||
|
||||
const match = result.parts.findLast((p) => p.type === "text")
|
||||
if (!match) throw new Error("Failed to parse the text response")
|
||||
const text = extractResponseText(result.parts)
|
||||
if (text) return text
|
||||
|
||||
return match.text
|
||||
// No text part (tool-only or reasoning-only) - ask agent to summarize
|
||||
console.log("Requesting summary from agent...")
|
||||
const summary = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
messageID: Identifier.ascending("message"),
|
||||
model: {
|
||||
providerID,
|
||||
modelID,
|
||||
},
|
||||
tools: { "*": false }, // Disable all tools to force text response
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
type: "text",
|
||||
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (summary.info.role === "assistant" && summary.info.error) {
|
||||
console.error(summary.info)
|
||||
throw new Error(
|
||||
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
const summaryText = extractResponseText(summary.parts)
|
||||
if (!summaryText) {
|
||||
throw new Error("Failed to get summary from agent")
|
||||
}
|
||||
|
||||
return summaryText
|
||||
}
|
||||
|
||||
async function getOidcToken() {
|
||||
|
||||
@@ -82,12 +82,21 @@ async function getAllSessions(): Promise<Session.Info[]> {
|
||||
return sessions
|
||||
}
|
||||
|
||||
async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
const sessions = await getAllSessions()
|
||||
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
|
||||
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
|
||||
const MS_IN_DAY = 24 * 60 * 60 * 1000
|
||||
|
||||
let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
||||
const cutoffTime = (() => {
|
||||
if (days === undefined) return 0
|
||||
if (days === 0) {
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
return now.getTime()
|
||||
}
|
||||
return Date.now() - days * MS_IN_DAY
|
||||
})()
|
||||
|
||||
let filteredSessions = cutoffTime > 0 ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
||||
|
||||
if (projectFilter !== undefined) {
|
||||
if (projectFilter === "") {
|
||||
@@ -198,7 +207,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND))
|
||||
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY))
|
||||
stats.dateRange = {
|
||||
earliest: earliestTime,
|
||||
latest: latestTime,
|
||||
|
||||
@@ -37,11 +37,9 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
const recents = local.model.recent()
|
||||
|
||||
const recentList = showExtra()
|
||||
? recents
|
||||
.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
)
|
||||
.slice(0, 5)
|
||||
? recents.filter(
|
||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||
)
|
||||
: []
|
||||
|
||||
const favoriteOptions = favorites.flatMap((item) => {
|
||||
@@ -182,7 +180,10 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
// Apply fuzzy filtering to each section separately, maintaining section order
|
||||
if (q) {
|
||||
const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj)
|
||||
const filteredRecents = fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj)
|
||||
const filteredRecents = fuzzysort
|
||||
.go(q, recentOptions, { keys: ["title"] })
|
||||
.map((x) => x.obj)
|
||||
.slice(0, 5)
|
||||
const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
|
||||
const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj)
|
||||
return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular]
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||
import { Link } from "../ui/link"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
|
||||
@@ -128,7 +129,7 @@ function AutoMethod(props: AutoMethodProps) {
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<Link href={props.authorization.url} fg={theme.primary} />
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
||||
@@ -170,7 +171,7 @@ function CodeMethod(props: CodeMethodProps) {
|
||||
description={() => (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||
<Link href={props.authorization.url} fg={theme.primary} />
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>Invalid code</text>
|
||||
</Show>
|
||||
|
||||
@@ -12,6 +12,38 @@ import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { Locale } from "@/util/locale"
|
||||
import type { PromptInfo } from "./history"
|
||||
|
||||
function removeLineRange(input: string) {
|
||||
const hashIndex = input.lastIndexOf("#")
|
||||
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
|
||||
}
|
||||
|
||||
function extractLineRange(input: string) {
|
||||
const hashIndex = input.lastIndexOf("#")
|
||||
if (hashIndex === -1) {
|
||||
return { baseQuery: input }
|
||||
}
|
||||
|
||||
const baseName = input.substring(0, hashIndex)
|
||||
const linePart = input.substring(hashIndex + 1)
|
||||
const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/)
|
||||
|
||||
if (!lineMatch) {
|
||||
return { baseQuery: baseName }
|
||||
}
|
||||
|
||||
const startLine = Number(lineMatch[1])
|
||||
const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined
|
||||
|
||||
return {
|
||||
lineRange: {
|
||||
baseName,
|
||||
startLine,
|
||||
endLine,
|
||||
},
|
||||
baseQuery: baseName,
|
||||
}
|
||||
}
|
||||
|
||||
export type AutocompleteRef = {
|
||||
onInput: (value: string) => void
|
||||
onKeyDown: (e: KeyEvent) => void
|
||||
@@ -142,9 +174,11 @@ export function Autocomplete(props: {
|
||||
async (query) => {
|
||||
if (!store.visible || store.visible === "/") return []
|
||||
|
||||
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
||||
|
||||
// Get files from SDK
|
||||
const result = await sdk.client.find.files({
|
||||
query: query ?? "",
|
||||
query: baseQuery,
|
||||
})
|
||||
|
||||
const options: AutocompleteOption[] = []
|
||||
@@ -153,15 +187,27 @@ export function Autocomplete(props: {
|
||||
if (!result.error && result.data) {
|
||||
const width = props.anchor().width - 4
|
||||
options.push(
|
||||
...result.data.map(
|
||||
(item): AutocompleteOption => ({
|
||||
display: Locale.truncateMiddle(item, width),
|
||||
...result.data.map((item): AutocompleteOption => {
|
||||
let url = `file://${process.cwd()}/${item}`
|
||||
let filename = item
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
const urlObj = new URL(url)
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
url = urlObj.toString()
|
||||
}
|
||||
|
||||
return {
|
||||
display: Locale.truncateMiddle(filename, width),
|
||||
onSelect: () => {
|
||||
insertPart(item, {
|
||||
insertPart(filename, {
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename: item,
|
||||
url: `file://${process.cwd()}/${item}`,
|
||||
filename,
|
||||
url,
|
||||
source: {
|
||||
type: "file",
|
||||
text: {
|
||||
@@ -173,8 +219,8 @@ export function Autocomplete(props: {
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -383,8 +429,8 @@ export function Autocomplete(props: {
|
||||
return prev
|
||||
}
|
||||
|
||||
const result = fuzzysort.go(currentFilter, mixed, {
|
||||
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
||||
keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||
limit: 10,
|
||||
scoreFn: (objResults) => {
|
||||
const displayResult = objResults[0]
|
||||
|
||||
@@ -52,7 +52,6 @@ import { DialogMessage } from "./dialog-message"
|
||||
import type { PromptInfo } from "../../component/prompt/history"
|
||||
import { iife } from "@/util/iife"
|
||||
import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
||||
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||
import { DialogTimeline } from "./dialog-timeline"
|
||||
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
|
||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||
@@ -67,7 +66,7 @@ import stripAnsi from "strip-ansi"
|
||||
import { Footer } from "./footer.tsx"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { DialogSubagent } from "./dialog-subagent.tsx"
|
||||
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -784,8 +783,22 @@ export function Session() {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text" && !part.synthetic) {
|
||||
transcript += `${part.text}\n\n`
|
||||
} else if (part.type === "reasoning") {
|
||||
if (showThinking()) {
|
||||
transcript += `_Thinking:_\n\n${part.text}\n\n`
|
||||
}
|
||||
} else if (part.type === "tool") {
|
||||
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
|
||||
transcript += `\`\`\`\nTool: ${part.tool}\n`
|
||||
if (showDetails() && part.state.input) {
|
||||
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
|
||||
}
|
||||
if (showDetails() && part.state.status === "completed" && part.state.output) {
|
||||
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
|
||||
}
|
||||
if (showDetails() && part.state.status === "error" && part.state.error) {
|
||||
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
|
||||
}
|
||||
transcript += `\n\`\`\`\n\n`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,6 +825,14 @@ export function Session() {
|
||||
const sessionData = session()
|
||||
const sessionMessages = messages()
|
||||
|
||||
const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
|
||||
|
||||
const options = await DialogExportOptions.show(dialog, defaultFilename, showThinking(), showDetails())
|
||||
|
||||
if (options === null) return
|
||||
|
||||
const { filename: customFilename, thinking: includeThinking, toolDetails: includeToolDetails } = options
|
||||
|
||||
let transcript = `# ${sessionData.title}\n\n`
|
||||
transcript += `**Session ID:** ${sessionData.id}\n`
|
||||
transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n`
|
||||
@@ -826,22 +847,28 @@ export function Session() {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text" && !part.synthetic) {
|
||||
transcript += `${part.text}\n\n`
|
||||
} else if (part.type === "reasoning") {
|
||||
if (includeThinking) {
|
||||
transcript += `_Thinking:_\n\n${part.text}\n\n`
|
||||
}
|
||||
} else if (part.type === "tool") {
|
||||
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
|
||||
transcript += `\`\`\`\nTool: ${part.tool}\n`
|
||||
if (includeToolDetails && part.state.input) {
|
||||
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
|
||||
}
|
||||
if (includeToolDetails && part.state.status === "completed" && part.state.output) {
|
||||
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
|
||||
}
|
||||
if (includeToolDetails && part.state.status === "error" && part.state.error) {
|
||||
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
|
||||
}
|
||||
transcript += `\n\`\`\`\n\n`
|
||||
}
|
||||
}
|
||||
|
||||
transcript += `---\n\n`
|
||||
}
|
||||
|
||||
// Prompt for optional filename
|
||||
const customFilename = await DialogPrompt.show(dialog, "Export filename", {
|
||||
value: `session-${sessionData.id.slice(0, 8)}.md`,
|
||||
})
|
||||
|
||||
// Cancel if user pressed escape
|
||||
if (customFilename === null) return
|
||||
|
||||
// Save to file in current working directory
|
||||
const exportDir = process.cwd()
|
||||
const filename = customFilename.trim()
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { onMount, Show, type JSX } from "solid-js"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
|
||||
export type DialogExportOptionsProps = {
|
||||
defaultFilename: string
|
||||
defaultThinking: boolean
|
||||
defaultToolDetails: boolean
|
||||
onConfirm?: (options: { filename: string; thinking: boolean; toolDetails: boolean }) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
let textarea: TextareaRenderable
|
||||
const [store, setStore] = createStore({
|
||||
thinking: props.defaultThinking,
|
||||
toolDetails: props.defaultToolDetails,
|
||||
active: "filename" as "filename" | "thinking" | "toolDetails",
|
||||
})
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "return") {
|
||||
props.onConfirm?.({
|
||||
filename: textarea.plainText,
|
||||
thinking: store.thinking,
|
||||
toolDetails: store.toolDetails,
|
||||
})
|
||||
}
|
||||
if (evt.name === "tab") {
|
||||
const order: Array<"filename" | "thinking" | "toolDetails"> = ["filename", "thinking", "toolDetails"]
|
||||
const currentIndex = order.indexOf(store.active)
|
||||
const nextIndex = (currentIndex + 1) % order.length
|
||||
setStore("active", order[nextIndex])
|
||||
evt.preventDefault()
|
||||
}
|
||||
if (evt.name === "space") {
|
||||
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
||||
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
|
||||
evt.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
}, 1)
|
||||
textarea.gotoLineEnd()
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
Export Options
|
||||
</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<box>
|
||||
<text fg={theme.text}>Filename:</text>
|
||||
</box>
|
||||
<textarea
|
||||
onSubmit={() => {
|
||||
props.onConfirm?.({
|
||||
filename: textarea.plainText,
|
||||
thinking: store.thinking,
|
||||
toolDetails: store.toolDetails,
|
||||
})
|
||||
}}
|
||||
height={3}
|
||||
keyBindings={[{ name: "return", action: "submit" }]}
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
initialValue={props.defaultFilename}
|
||||
placeholder="Enter filename"
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.text}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="column">
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "thinking")}
|
||||
>
|
||||
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
|
||||
{store.thinking ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "toolDetails")}
|
||||
>
|
||||
<text fg={store.active === "toolDetails" ? theme.primary : theme.textMuted}>
|
||||
{store.toolDetails ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "toolDetails" ? theme.primary : theme.text}>Include tool details</text>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={store.active !== "filename"}>
|
||||
<text fg={theme.textMuted} paddingBottom={1}>
|
||||
Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
|
||||
to confirm
|
||||
</text>
|
||||
</Show>
|
||||
<Show when={store.active === "filename"}>
|
||||
<text fg={theme.textMuted} paddingBottom={1}>
|
||||
Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
|
||||
for options
|
||||
</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogExportOptions.show = (
|
||||
dialog: DialogContext,
|
||||
defaultFilename: string,
|
||||
defaultThinking: boolean,
|
||||
defaultToolDetails: boolean,
|
||||
) => {
|
||||
return new Promise<{ filename: string; thinking: boolean; toolDetails: boolean } | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogExportOptions
|
||||
defaultFilename={defaultFilename}
|
||||
defaultThinking={defaultThinking}
|
||||
defaultToolDetails={defaultToolDetails}
|
||||
onConfirm={(options) => resolve(options)}
|
||||
onCancel={() => resolve(null)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import open from "open"
|
||||
|
||||
export interface LinkProps {
|
||||
href: string
|
||||
children?: JSX.Element | string
|
||||
fg?: RGBA
|
||||
}
|
||||
|
||||
/**
|
||||
* Link component that renders clickable hyperlinks.
|
||||
* Clicking anywhere on the link text opens the URL in the default browser.
|
||||
*/
|
||||
export function Link(props: LinkProps) {
|
||||
const displayText = props.children ?? props.href
|
||||
|
||||
return (
|
||||
<text
|
||||
fg={props.fg}
|
||||
onMouseUp={() => {
|
||||
open(props.href).catch(() => {})
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
</text>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export namespace Config {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
|
||||
// to satisy the type checker
|
||||
// to satisfy the type checker
|
||||
result.agent ??= {}
|
||||
result.mode ??= {}
|
||||
result.plugin ??= []
|
||||
@@ -155,7 +155,7 @@ export namespace Config {
|
||||
}
|
||||
})
|
||||
|
||||
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools"].join(",")}}/`)
|
||||
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skills"].join(",")}}/`)
|
||||
async function assertValid(dir: string) {
|
||||
const invalid = await Array.fromAsync(
|
||||
INVALID_DIRS.scan({
|
||||
@@ -191,6 +191,10 @@ export namespace Config {
|
||||
cwd: dir,
|
||||
},
|
||||
).catch(() => {})
|
||||
|
||||
// Install any additional dependencies defined in the package.json
|
||||
// This allows local plugins and custom tools to use external packages
|
||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||
}
|
||||
|
||||
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
||||
|
||||
@@ -77,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
})
|
||||
})
|
||||
.usage("\n" + UI.logo())
|
||||
.completion("completion", "generate shell completion script")
|
||||
.command(AcpCommand)
|
||||
.command(McpCommand)
|
||||
.command(TuiThreadCommand)
|
||||
|
||||
@@ -196,13 +196,14 @@ export namespace LSPServer {
|
||||
}
|
||||
await fs.rename(extractedPath, finalPath)
|
||||
|
||||
await $`npm install`.cwd(finalPath).quiet()
|
||||
await $`npm run compile`.cwd(finalPath).quiet()
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||
await $`${npmCmd} install`.cwd(finalPath).quiet()
|
||||
await $`${npmCmd} run compile`.cwd(finalPath).quiet()
|
||||
|
||||
log.info("installed VS Code ESLint server", { serverPath })
|
||||
}
|
||||
|
||||
const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
|
||||
const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -308,10 +308,12 @@ export namespace MCP {
|
||||
|
||||
if (mcp.type === "local") {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const cwd = Instance.directory
|
||||
const transport = new StdioClientTransport({
|
||||
stderr: "ignore",
|
||||
command: cmd,
|
||||
args,
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
||||
@@ -334,6 +336,7 @@ export namespace MCP {
|
||||
log.error("local mcp startup failed", {
|
||||
key,
|
||||
command: mcp.command,
|
||||
cwd,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
status = {
|
||||
|
||||
@@ -165,29 +165,44 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
|
||||
Env.get("AWS_PROFILE"),
|
||||
Env.get("AWS_ACCESS_KEY_ID"),
|
||||
Env.get("AWS_BEARER_TOKEN_BEDROCK"),
|
||||
Env.get("AWS_REGION"),
|
||||
])
|
||||
const auth = await Auth.get("amazon-bedrock")
|
||||
const awsProfile = Env.get("AWS_PROFILE")
|
||||
const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
|
||||
const awsRegion = Env.get("AWS_REGION")
|
||||
|
||||
const awsBearerToken = iife(() => {
|
||||
const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")
|
||||
if (envToken) return envToken
|
||||
if (auth?.type === "api") {
|
||||
Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)
|
||||
return auth.key
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
|
||||
|
||||
const region = awsRegion ?? "us-east-1"
|
||||
const defaultRegion = awsRegion ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
region,
|
||||
region: defaultRegion,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
},
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
// Skip region prefixing if model already has global prefix
|
||||
if (modelID.startsWith("global.")) {
|
||||
return sdk.languageModel(modelID)
|
||||
}
|
||||
|
||||
// Region resolution precedence (highest to lowest):
|
||||
// 1. options.region from opencode.json provider config
|
||||
// 2. defaultRegion from AWS_REGION environment variable
|
||||
// 3. Default "us-east-1" (baked into defaultRegion)
|
||||
const region = options?.region ?? defaultRegion
|
||||
|
||||
let regionPrefix = region.split("-")[0]
|
||||
|
||||
switch (regionPrefix) {
|
||||
|
||||
@@ -68,7 +68,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
|
||||
- After fetching, review the content returned by the fetch tool.
|
||||
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
|
||||
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
|
||||
- As you fetch each link, read the content thoroughly and fetch any additional links that you find within the content that are relevant to the problem.
|
||||
- Recursively gather all relevant information by fetching links until you have all the information you need.
|
||||
|
||||
## 5. Develop a Detailed Plan
|
||||
|
||||
@@ -222,7 +222,7 @@ The messages you send before tool calls should describe what is immediately abou
|
||||
|
||||
Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges.
|
||||
|
||||
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multi-section structured responses for results that need grouping or explanation.
|
||||
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multisection structured responses for results that need grouping or explanation.
|
||||
|
||||
The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `edit`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path.
|
||||
|
||||
@@ -259,7 +259,7 @@ You are producing plain text that will later be styled by the CLI. Follow these
|
||||
**File References**
|
||||
When referencing files in your response, make sure to include the relevant start line and always follow the below rules:
|
||||
* Use inline code to make file paths clickable.
|
||||
* Each reference should have a stand alone path. Even if it's the same file.
|
||||
* Each reference should have a standalone path. Even if it's the same file.
|
||||
* Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.
|
||||
* Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
|
||||
* Do not use URIs like file://, vscode://, or https://.
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Ripgrep } from "../file/ripgrep"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Config } from "../config/config"
|
||||
import { Skill } from "../skill"
|
||||
|
||||
import { Instance } from "../project/instance"
|
||||
import path from "path"
|
||||
|
||||
@@ -32,44 +32,59 @@ export namespace Skill {
|
||||
}),
|
||||
)
|
||||
|
||||
const SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const directories = await Config.directories()
|
||||
const skills: Record<string, Info> = {}
|
||||
|
||||
const addSkill = async (match: string) => {
|
||||
const md = await ConfigMarkdown.parse(match)
|
||||
if (!md) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
||||
if (!parsed.success) return
|
||||
|
||||
// Warn on duplicate skill names
|
||||
if (skills[parsed.data.name]) {
|
||||
log.warn("duplicate skill name", {
|
||||
name: parsed.data.name,
|
||||
existing: skills[parsed.data.name].location,
|
||||
duplicate: match,
|
||||
})
|
||||
}
|
||||
|
||||
skills[parsed.data.name] = {
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
location: match,
|
||||
}
|
||||
}
|
||||
|
||||
for (const dir of directories) {
|
||||
for await (const match of SKILL_GLOB.scan({
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(match)
|
||||
if (!md) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
||||
if (!parsed.success) continue
|
||||
|
||||
// Warn on duplicate skill names
|
||||
if (skills[parsed.data.name]) {
|
||||
log.warn("duplicate skill name", {
|
||||
name: parsed.data.name,
|
||||
existing: skills[parsed.data.name].location,
|
||||
duplicate: match,
|
||||
})
|
||||
}
|
||||
|
||||
skills[parsed.data.name] = {
|
||||
name: parsed.data.name,
|
||||
description: parsed.data.description,
|
||||
location: match,
|
||||
}
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: Instance.worktree,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
|
||||
return skills
|
||||
})
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
- Returns matching file paths sorted by modification time
|
||||
- Use this tool when you need to find files by name patterns
|
||||
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
||||
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
||||
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
|
||||
|
||||
@@ -26,20 +26,25 @@ export const SkillTool: Tool.Info<typeof parameters> = {
|
||||
})
|
||||
}
|
||||
|
||||
const description =
|
||||
accessibleSkills.length === 0
|
||||
? "Load a skill to get detailed instructions for a specific task. No skills are currently available."
|
||||
: [
|
||||
"Load a skill to get detailed instructions for a specific task.",
|
||||
"Skills provide specialized knowledge and step-by-step guidance.",
|
||||
"Use this when a task matches an available skill's description.",
|
||||
"<available_skills>",
|
||||
...accessibleSkills.flatMap((skill) => [
|
||||
` <skill>`,
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
` </skill>`,
|
||||
]),
|
||||
"</available_skills>",
|
||||
].join(" ")
|
||||
|
||||
return {
|
||||
description: [
|
||||
"Load a skill to get detailed instructions for a specific task.",
|
||||
"Skills provide specialized knowledge and step-by-step guidance.",
|
||||
"Use this when a task matches an available skill's description.",
|
||||
"<available_skills>",
|
||||
...accessibleSkills.flatMap((skill) => [
|
||||
` <skill>`,
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
` </skill>`,
|
||||
]),
|
||||
"</available_skills>",
|
||||
].join(" "),
|
||||
description,
|
||||
parameters,
|
||||
async execute(params, ctx) {
|
||||
const agent = await Agent.get(ctx.agent)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { extractResponseText } from "../../src/cli/cmd/github"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
// Helper to create minimal valid parts
|
||||
function createTextPart(text: string): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
type: "text" as const,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
function createReasoningPart(text: string): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
type: "reasoning" as const,
|
||||
text,
|
||||
time: { start: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
|
||||
if (status === "completed") {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
type: "tool" as const,
|
||||
callID: "c1",
|
||||
tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "",
|
||||
title,
|
||||
metadata: {},
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
type: "tool" as const,
|
||||
callID: "c1",
|
||||
tool,
|
||||
state: {
|
||||
status: "running",
|
||||
input: {},
|
||||
time: { start: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createStepStartPart(): MessageV2.Part {
|
||||
return {
|
||||
id: "1",
|
||||
sessionID: "s",
|
||||
messageID: "m",
|
||||
type: "step-start" as const,
|
||||
}
|
||||
}
|
||||
|
||||
describe("extractResponseText", () => {
|
||||
test("returns text from text part", () => {
|
||||
const parts = [createTextPart("Hello world")]
|
||||
expect(extractResponseText(parts)).toBe("Hello world")
|
||||
})
|
||||
|
||||
test("returns last text part when multiple exist", () => {
|
||||
const parts = [createTextPart("First"), createTextPart("Last")]
|
||||
expect(extractResponseText(parts)).toBe("Last")
|
||||
})
|
||||
|
||||
test("returns text even when tool parts follow", () => {
|
||||
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
|
||||
expect(extractResponseText(parts)).toBe("I'll help with that.")
|
||||
})
|
||||
|
||||
test("returns null for reasoning-only response (signals summary needed)", () => {
|
||||
const parts = [createReasoningPart("Let me think about this...")]
|
||||
expect(extractResponseText(parts)).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null for tool-only response (signals summary needed)", () => {
|
||||
// This is the exact scenario from the bug report - todowrite with no text
|
||||
const parts = [createToolPart("todowrite", "8 todos")]
|
||||
expect(extractResponseText(parts)).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null for multiple completed tools", () => {
|
||||
const parts = [
|
||||
createToolPart("read", "src/file.ts"),
|
||||
createToolPart("edit", "src/file.ts"),
|
||||
createToolPart("bash", "bun test"),
|
||||
]
|
||||
expect(extractResponseText(parts)).toBeNull()
|
||||
})
|
||||
|
||||
test("ignores running tool parts (throws since no completed tools)", () => {
|
||||
const parts = [createToolPart("bash", "", "running")]
|
||||
expect(() => extractResponseText(parts)).toThrow("Failed to parse response")
|
||||
})
|
||||
|
||||
test("throws with part types on empty array", () => {
|
||||
expect(() => extractResponseText([])).toThrow("Part types found: [none]")
|
||||
})
|
||||
|
||||
test("throws with part types on unhandled parts", () => {
|
||||
const parts = [createStepStartPart()]
|
||||
expect(() => extractResponseText(parts)).toThrow("Part types found: [step-start]")
|
||||
})
|
||||
|
||||
test("prefers text over reasoning when both present", () => {
|
||||
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
|
||||
expect(extractResponseText(parts)).toBe("Final answer")
|
||||
})
|
||||
|
||||
test("prefers text over tools when both present", () => {
|
||||
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
|
||||
expect(extractResponseText(parts)).toBe("Here's what I found")
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,7 @@ but this is not:
|
||||
|
||||
We also need to deal with files followed by @commas, ones
|
||||
with @file-extensions.md, even @multiple.extensions.bak,
|
||||
hidden directorys like @.config/ or files like @.bashrc
|
||||
hidden directories like @.config/ or files like @.bashrc
|
||||
and ones at the end of a sentence like @foo.md.
|
||||
|
||||
Also shouldn't forget @/absolute/paths.txt with and @/without/extensions,
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Provider } from "../../src/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Global } from "../../src/global"
|
||||
|
||||
test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_REGION", "us-east-1")
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
// Region from config should be used (not env var)
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: falls back to AWS_REGION env var when no config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_REGION", "eu-west-1")
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: without explicit region config, uses AWS_REGION env or defaults", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
// AWS_REGION might be set in the environment, use that or default
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
// Should have some region set (either from env or default)
|
||||
expect(providers["amazon-bedrock"].options?.region).toBeDefined()
|
||||
expect(typeof providers["amazon-bedrock"].options?.region).toBe("string")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: uses config region in provider options", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-north-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const bedrockProvider = providers["amazon-bedrock"]
|
||||
expect(bedrockProvider).toBeDefined()
|
||||
expect(bedrockProvider.options?.region).toBe("eu-north-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: respects config region for different instances", async () => {
|
||||
// First instance with EU config
|
||||
await using tmp1 = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp1.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
Env.set("AWS_REGION", "us-east-1")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers1 = await Provider.list()
|
||||
expect(providers1["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
|
||||
// Second instance with US config
|
||||
await using tmp2 = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "us-west-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp2.path,
|
||||
init: async () => {
|
||||
Env.set("AWS_PROFILE", "default")
|
||||
Env.set("AWS_REGION", "eu-west-1")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers2 = await Provider.list()
|
||||
expect(providers2["amazon-bedrock"].options?.region).toBe("us-west-2")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
provider: {
|
||||
"amazon-bedrock": {
|
||||
options: {
|
||||
region: "eu-west-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Setup auth.json with bearer token for amazon-bedrock
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
await Bun.write(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
"amazon-bedrock": {
|
||||
type: "api",
|
||||
key: "test-bearer-token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
// Clear env vars so only auth.json should trigger autoload
|
||||
Env.set("AWS_PROFILE", "")
|
||||
Env.set("AWS_ACCESS_KEY_ID", "")
|
||||
Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -101,31 +101,31 @@ test("returns empty array when no skills exist", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
// test("discovers skills from .claude/skills/ directory", async () => {
|
||||
// await using tmp = await tmpdir({
|
||||
// git: true,
|
||||
// init: async (dir) => {
|
||||
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
// await Bun.write(
|
||||
// path.join(skillDir, "SKILL.md"),
|
||||
// `---
|
||||
// name: claude-skill
|
||||
// description: A skill in the .claude/skills directory.
|
||||
// ---
|
||||
test("discovers skills from .claude/skills/ directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||
await Bun.write(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
`---
|
||||
name: claude-skill
|
||||
description: A skill in the .claude/skills directory.
|
||||
---
|
||||
|
||||
// # Claude Skill
|
||||
// `,
|
||||
// )
|
||||
// },
|
||||
// })
|
||||
# Claude Skill
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// await Instance.provide({
|
||||
// directory: tmp.path,
|
||||
// fn: async () => {
|
||||
// const skills = await Skill.all()
|
||||
// expect(skills.length).toBe(1)
|
||||
// expect(skills[0].name).toBe("claude-skill")
|
||||
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.204",
|
||||
"version": "1.0.208",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
@@ -11,6 +11,9 @@
|
||||
"./context/*": "./src/context/*.tsx",
|
||||
"./styles": "./src/styles/index.css",
|
||||
"./styles/tailwind": "./src/styles/tailwind/index.css",
|
||||
"./theme": "./src/theme/index.ts",
|
||||
"./theme/*": "./src/theme/*.ts",
|
||||
"./theme/context": "./src/theme/context.tsx",
|
||||
"./icons/provider": "./src/components/provider-icons/types.ts",
|
||||
"./icons/file-type": "./src/components/file-icons/types.ts",
|
||||
"./fonts/*": "./src/assets/fonts/*",
|
||||
@@ -22,15 +25,16 @@
|
||||
"generate:tailwind": "bun run script/tailwind.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/luxon": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
"vite-plugin-solid": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:"
|
||||
"vite-plugin-solid": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -43,8 +47,10 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-katex-extension": "5.1.6",
|
||||
"marked-shiki": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
[data-component="image-preview"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="image-preview-container"] {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: min(calc(100vw - 32px), 90vw);
|
||||
max-width: 1200px;
|
||||
height: min(calc(100vh - 32px), 90vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="image-preview-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
box-shadow:
|
||||
0 15px 45px 0 rgba(19, 16, 16, 0.35),
|
||||
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
|
||||
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-header"] {
|
||||
display: flex;
|
||||
padding: 8px 8px 0;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-body"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-image"] {
|
||||
max-width: 100%;
|
||||
max-height: calc(90vh - 100px);
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
export interface ImagePreviewProps {
|
||||
src: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export function ImagePreview(props: ImagePreviewProps) {
|
||||
return (
|
||||
<div data-component="image-preview">
|
||||
<div data-slot="image-preview-container">
|
||||
<Kobalte.Content data-slot="image-preview-content">
|
||||
<div data-slot="image-preview-header">
|
||||
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
|
||||
</div>
|
||||
<div data-slot="image-preview-body">
|
||||
<img src={props.src} alt={props.alt ?? "Image preview"} data-slot="image-preview-image" />
|
||||
</div>
|
||||
</Kobalte.Content>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { createEffect, createSignal, For, type JSX, on, Show } from "solid-js"
|
||||
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Icon, type IconProps } from "./icon"
|
||||
import { IconButton } from "./icon-button"
|
||||
@@ -15,6 +15,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
|
||||
children: (item: T) => JSX.Element
|
||||
emptyMessage?: string
|
||||
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
|
||||
onMove?: (item: T | undefined) => void
|
||||
activeIcon?: IconProps["name"]
|
||||
filter?: string
|
||||
search?: ListSearchProps | boolean
|
||||
@@ -82,6 +83,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
||||
element?.scrollIntoView({ block: "center", behavior: "smooth" })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const all = flat()
|
||||
const current = active()
|
||||
const item = all.find((x) => props.key(x) === current)
|
||||
props.onMove?.(item)
|
||||
})
|
||||
|
||||
const handleSelect = (item: T | undefined, index: number) => {
|
||||
props.onSelect?.(item, index)
|
||||
}
|
||||
@@ -108,6 +116,33 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
||||
setScrollRef,
|
||||
})
|
||||
|
||||
function GroupHeader(props: { category: string }): JSX.Element {
|
||||
const [stuck, setStuck] = createSignal(false)
|
||||
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||
|
||||
createEffect(() => {
|
||||
const scroll = scrollRef()
|
||||
const node = header()
|
||||
if (!scroll || !node) return
|
||||
|
||||
const handler = () => {
|
||||
const rect = node.getBoundingClientRect()
|
||||
const scrollRect = scroll.getBoundingClientRect()
|
||||
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
|
||||
}
|
||||
|
||||
scroll.addEventListener("scroll", handler, { passive: true })
|
||||
handler()
|
||||
onCleanup(() => scroll.removeEventListener("scroll", handler))
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}>
|
||||
{props.category}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
||||
<Show when={!!props.search}>
|
||||
@@ -149,31 +184,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
||||
{(group) => (
|
||||
<div data-slot="list-group">
|
||||
<Show when={group.category}>
|
||||
{(() => {
|
||||
const [stuck, setStuck] = createSignal(false)
|
||||
return (
|
||||
<div
|
||||
data-slot="list-header"
|
||||
data-stuck={stuck()}
|
||||
ref={(el) => {
|
||||
createEffect(() => {
|
||||
const scroll = scrollRef()
|
||||
if (!scroll) return
|
||||
const handler = () => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const scrollRect = scroll.getBoundingClientRect()
|
||||
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
|
||||
}
|
||||
scroll.addEventListener("scroll", handler, { passive: true })
|
||||
handler()
|
||||
return () => scroll.removeEventListener("scroll", handler)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{group.category}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<GroupHeader category={group.category} />
|
||||
</Show>
|
||||
<div data-slot="list-items">
|
||||
<For each={group.items}>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
color: var(--text-base);
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base); /* 14px */
|
||||
line-height: var(--line-height-large);
|
||||
line-height: var(--line-height-x-large);
|
||||
|
||||
/* Spacing for flow */
|
||||
> *:first-child {
|
||||
@@ -64,8 +64,16 @@
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
li::marker {
|
||||
@@ -117,13 +125,15 @@
|
||||
:not(pre) > code {
|
||||
font-family: var(--font-family-mono);
|
||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
font-size: 13px;
|
||||
color: var(--syntax-string);
|
||||
font-weight: var(--font-weight-medium);
|
||||
/* font-size: 13px; */
|
||||
|
||||
padding: 2px 2px;
|
||||
margin: 0 1.5px;
|
||||
border-radius: 2px;
|
||||
background: var(--surface-base);
|
||||
box-shadow: 0 0 0 0.5px var(--border-weak-base);
|
||||
/* padding: 2px 2px; */
|
||||
/* margin: 0 1.5px; */
|
||||
/* border-radius: 2px; */
|
||||
/* background: var(--surface-base); */
|
||||
/* box-shadow: 0 0 0 0.5px var(--border-weak-base); */
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
border-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
&[data-clickable="true"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-type="image"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -137,6 +141,9 @@
|
||||
|
||||
[data-slot="message-part-tool-error-message"] {
|
||||
color: var(--text-on-critical-weak);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +249,14 @@
|
||||
|
||||
[data-component="edit-content"] {
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="write-content"] {
|
||||
@@ -367,9 +382,9 @@
|
||||
|
||||
&[data-permission="true"] {
|
||||
position: sticky;
|
||||
top: var(--sticky-header-height, 80px);
|
||||
top: calc(2px + var(--sticky-header-height, 40px));
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
z-index: 20;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
@@ -380,6 +395,7 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1.5px;
|
||||
top: -5px;
|
||||
border-radius: 7.5px;
|
||||
border: 1.5px solid transparent;
|
||||
background:
|
||||
@@ -387,12 +403,12 @@
|
||||
conic-gradient(
|
||||
from var(--border-angle),
|
||||
transparent 0deg,
|
||||
transparent 270deg,
|
||||
transparent 0deg,
|
||||
var(--border-warning-strong, var(--border-warning-selected)) 300deg,
|
||||
var(--border-warning-base) 360deg
|
||||
)
|
||||
border-box;
|
||||
animation: chase-border 1.5s linear infinite;
|
||||
animation: chase-border 2.5s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Component, createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
onCleanup,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import {
|
||||
AssistantMessage,
|
||||
@@ -14,6 +25,7 @@ import {
|
||||
import { useData } from "../context"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useCodeComponent } from "../context/code"
|
||||
import { useDialog } from "../context/dialog"
|
||||
import { BasicTool } from "./basic-tool"
|
||||
import { GenericTool } from "./basic-tool"
|
||||
import { Button } from "./button"
|
||||
@@ -22,6 +34,7 @@ import { Icon } from "./icon"
|
||||
import { Checkbox } from "./checkbox"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Markdown } from "./markdown"
|
||||
import { ImagePreview } from "./image-preview"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { createAutoScroll } from "../hooks"
|
||||
@@ -80,6 +93,47 @@ export type PartComponent = Component<MessagePartProps>
|
||||
|
||||
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
||||
|
||||
const TEXT_RENDER_THROTTLE_MS = 100
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
function createThrottledValue(getValue: () => string) {
|
||||
const [value, setValue] = createSignal(getValue())
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
let last = 0
|
||||
|
||||
createEffect(() => {
|
||||
const next = getValue()
|
||||
const now = Date.now()
|
||||
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
||||
if (remaining <= 0) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
}
|
||||
last = now
|
||||
setValue(next)
|
||||
return
|
||||
}
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
last = Date.now()
|
||||
setValue(next)
|
||||
timeout = undefined
|
||||
}, remaining)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function relativizeProjectPaths(text: string, directory?: string) {
|
||||
if (!text) return ""
|
||||
if (!directory) return text
|
||||
@@ -209,15 +263,21 @@ export function Message(props: MessageProps) {
|
||||
}
|
||||
|
||||
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
|
||||
const filteredParts = createMemo(() => {
|
||||
return props.parts?.filter((x) => {
|
||||
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
||||
})
|
||||
})
|
||||
const emptyParts: PartType[] = []
|
||||
const filteredParts = createMemo(
|
||||
() =>
|
||||
props.parts.filter((x) => {
|
||||
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
||||
}),
|
||||
emptyParts,
|
||||
{ equals: same },
|
||||
)
|
||||
return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
||||
}
|
||||
|
||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||
const dialog = useDialog()
|
||||
|
||||
const textPart = createMemo(
|
||||
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
||||
)
|
||||
@@ -240,13 +300,26 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
}),
|
||||
)
|
||||
|
||||
const openImagePreview = (url: string, alt?: string) => {
|
||||
dialog.show(() => <ImagePreview src={url} alt={alt} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="user-message">
|
||||
<Show when={attachments().length > 0}>
|
||||
<div data-slot="user-message-attachments">
|
||||
<For each={attachments()}>
|
||||
{(file) => (
|
||||
<div data-slot="user-message-attachment" data-type={file.mime.startsWith("image/") ? "image" : "file"}>
|
||||
<div
|
||||
data-slot="user-message-attachment"
|
||||
data-type={file.mime.startsWith("image/") ? "image" : "file"}
|
||||
data-clickable={file.mime.startsWith("image/") && !!file.url}
|
||||
onClick={() => {
|
||||
if (file.mime.startsWith("image/") && file.url) {
|
||||
openImagePreview(file.url, file.filename)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={file.mime.startsWith("image/") && file.url}
|
||||
fallback={
|
||||
@@ -362,9 +435,22 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
const part = props.part as ToolPart
|
||||
|
||||
const permission = createMemo(() => {
|
||||
const sessionID = props.message.sessionID
|
||||
const permissions = data.store.permission?.[sessionID] ?? []
|
||||
return permissions.find((p) => p.callID === part.callID)
|
||||
const next = data.store.permission?.[props.message.sessionID]?.[0]
|
||||
if (!next) return undefined
|
||||
if (next.callID !== part.callID) return undefined
|
||||
return next
|
||||
})
|
||||
|
||||
const [showPermission, setShowPermission] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
const perm = permission()
|
||||
if (perm) {
|
||||
const timeout = setTimeout(() => setShowPermission(true), 50)
|
||||
onCleanup(() => clearTimeout(timeout))
|
||||
} else {
|
||||
setShowPermission(false)
|
||||
}
|
||||
})
|
||||
|
||||
const [forceOpen, setForceOpen] = createSignal(false)
|
||||
@@ -382,13 +468,17 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
})
|
||||
}
|
||||
|
||||
const component = createMemo(() => {
|
||||
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
||||
// @ts-expect-error
|
||||
const metadata = part.state?.metadata ?? {}
|
||||
const input = part.state?.input ?? {}
|
||||
const emptyInput: Record<string, any> = {}
|
||||
const emptyMetadata: Record<string, any> = {}
|
||||
|
||||
return (
|
||||
const input = () => part.state?.input ?? emptyInput
|
||||
// @ts-expect-error
|
||||
const metadata = () => part.state?.metadata ?? emptyMetadata
|
||||
|
||||
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
||||
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-permission={showPermission()}>
|
||||
<Switch>
|
||||
<Match when={part.state.status === "error" && part.state.error}>
|
||||
{(error) => {
|
||||
@@ -417,9 +507,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
<Match when={true}>
|
||||
<Dynamic
|
||||
component={render}
|
||||
input={input}
|
||||
input={input()}
|
||||
tool={part.tool}
|
||||
metadata={metadata}
|
||||
metadata={metadata()}
|
||||
// @ts-expect-error
|
||||
output={part.state.output}
|
||||
status={part.state.status}
|
||||
@@ -429,13 +519,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-permission={!!permission()}>
|
||||
<Show when={component()}>{component()}</Show>
|
||||
<Show when={permission()}>
|
||||
<Show when={showPermission() && permission()}>
|
||||
{(perm) => (
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-message">{perm().title}</div>
|
||||
@@ -461,11 +545,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
const data = useData()
|
||||
const part = props.part as TextPart
|
||||
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
|
||||
const throttledText = createThrottledValue(displayText)
|
||||
|
||||
return (
|
||||
<Show when={displayText()}>
|
||||
<Show when={throttledText()}>
|
||||
<div data-component="text-part">
|
||||
<Markdown text={displayText()} />
|
||||
<Markdown text={throttledText()} />
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
@@ -473,10 +558,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
|
||||
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
||||
const part = props.part as ReasoningPart
|
||||
const text = () => part.text.trim()
|
||||
const throttledText = createThrottledValue(text)
|
||||
|
||||
return (
|
||||
<Show when={part.text.trim()}>
|
||||
<Show when={throttledText()}>
|
||||
<div data-component="reasoning-part">
|
||||
<Markdown text={part.text.trim()} />
|
||||
<Markdown text={throttledText()} />
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
@@ -623,7 +711,7 @@ ToolRegistry.register({
|
||||
const sessionId = childSessionId()
|
||||
if (!sessionId) return undefined
|
||||
const permissions = data.store.permission?.[sessionId] ?? []
|
||||
return permissions.toSorted((a, b) => a.id.localeCompare(b.id))[0]
|
||||
return permissions[0]
|
||||
})
|
||||
|
||||
const childToolPart = createMemo(() => {
|
||||
@@ -807,19 +895,19 @@ ToolRegistry.register({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={props.metadata.filediff}>
|
||||
<Show when={props.metadata.filediff?.path || props.input.filePath}>
|
||||
<div data-component="edit-content">
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
before={{
|
||||
name: props.metadata.filediff.path,
|
||||
contents: props.metadata.filediff.before,
|
||||
cacheKey: checksum(props.metadata.filediff.before),
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.before || props.input.oldString,
|
||||
cacheKey: checksum(props.metadata?.filediff?.before || props.input.oldString),
|
||||
}}
|
||||
after={{
|
||||
name: props.metadata.filediff.path,
|
||||
contents: props.metadata.filediff.after,
|
||||
cacheKey: checksum(props.metadata.filediff.after),
|
||||
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||
contents: props.metadata?.filediff?.after || props.input.newString,
|
||||
cacheKey: checksum(props.metadata?.filediff?.after || props.input.newString),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -343,6 +343,8 @@
|
||||
|
||||
.error-card {
|
||||
color: var(--text-on-critical-base);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible-content-inner"] {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
AssistantMessage,
|
||||
Message as MessageType,
|
||||
Part as PartType,
|
||||
type Permission,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { useData } from "../context"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Typewriter } from "./typewriter"
|
||||
@@ -60,26 +68,38 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
function AssistantMessageItem(props: {
|
||||
message: AssistantMessage
|
||||
summary: string | undefined
|
||||
response: string | undefined
|
||||
lastTextPartId: string | undefined
|
||||
working: boolean
|
||||
responsePartId: string | undefined
|
||||
hideResponsePart: boolean
|
||||
}) {
|
||||
const data = useData()
|
||||
const msgParts = createMemo(() => data.store.part[props.message.id] ?? [])
|
||||
const lastTextPart = createMemo(() =>
|
||||
msgParts()
|
||||
.filter((p) => p?.type === "text")
|
||||
.at(-1),
|
||||
)
|
||||
const emptyParts: PartType[] = []
|
||||
const msgParts = createMemo(() => data.store.part[props.message.id] ?? emptyParts)
|
||||
const lastTextPart = createMemo(() => {
|
||||
const parts = msgParts()
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const part = parts[i]
|
||||
if (part?.type === "text") return part as TextPart
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const filteredParts = createMemo(() => {
|
||||
if (!props.working && !props.summary && props.response && props.lastTextPartId === lastTextPart()?.id) {
|
||||
return msgParts().filter((p) => p?.id !== lastTextPart()?.id)
|
||||
}
|
||||
return msgParts()
|
||||
const parts = msgParts()
|
||||
if (!props.hideResponsePart) return parts
|
||||
|
||||
const responsePartId = props.responsePartId
|
||||
if (!responsePartId) return parts
|
||||
if (responsePartId !== lastTextPart()?.id) return parts
|
||||
|
||||
return parts.filter((part) => part?.id !== responsePartId)
|
||||
})
|
||||
|
||||
return <Message message={props.message} parts={filteredParts()} />
|
||||
@@ -89,6 +109,7 @@ export function SessionTurn(
|
||||
props: ParentProps<{
|
||||
sessionID: string
|
||||
messageID: string
|
||||
lastUserMessageID?: string
|
||||
stepsExpanded?: boolean
|
||||
onStepsExpandedToggle?: () => void
|
||||
onUserInteracted?: () => void
|
||||
@@ -102,27 +123,76 @@ export function SessionTurn(
|
||||
const data = useData()
|
||||
const diffComponent = useDiffComponent()
|
||||
|
||||
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? [])
|
||||
const userMessages = createMemo(() =>
|
||||
allMessages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
const emptyMessages: MessageType[] = []
|
||||
const emptyParts: PartType[] = []
|
||||
const emptyAssistant: AssistantMessage[] = []
|
||||
const emptyPermissions: Permission[] = []
|
||||
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID))
|
||||
const isLastUserMessage = createMemo(() => message()?.id === userMessages().at(-1)?.id)
|
||||
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages)
|
||||
|
||||
const messageIndex = createMemo(() => {
|
||||
const messages = allMessages()
|
||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||
if (!result.found) return -1
|
||||
|
||||
const msg = messages[result.index]
|
||||
if (msg.role !== "user") return -1
|
||||
|
||||
return result.index
|
||||
})
|
||||
|
||||
const message = createMemo(() => {
|
||||
const index = messageIndex()
|
||||
if (index < 0) return undefined
|
||||
|
||||
const msg = allMessages()[index]
|
||||
if (!msg || msg.role !== "user") return undefined
|
||||
|
||||
return msg
|
||||
})
|
||||
|
||||
const lastUserMessageID = createMemo(() => {
|
||||
if (props.lastUserMessageID) return props.lastUserMessageID
|
||||
|
||||
const messages = allMessages()
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i]
|
||||
if (msg?.role === "user") return msg.id
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const msg = message()
|
||||
if (!msg) return []
|
||||
return data.store.part[msg.id] ?? []
|
||||
if (!msg) return emptyParts
|
||||
return data.store.part[msg.id] ?? emptyParts
|
||||
})
|
||||
|
||||
const assistantMessages = createMemo(() => {
|
||||
const msg = message()
|
||||
if (!msg) return [] as AssistantMessage[]
|
||||
return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
|
||||
})
|
||||
const assistantMessages = createMemo(
|
||||
() => {
|
||||
const msg = message()
|
||||
if (!msg) return emptyAssistant
|
||||
|
||||
const messages = allMessages()
|
||||
const index = messageIndex()
|
||||
if (index < 0) return emptyAssistant
|
||||
|
||||
const result: AssistantMessage[] = []
|
||||
for (let i = index + 1; i < messages.length; i++) {
|
||||
const item = messages[i]
|
||||
if (!item) continue
|
||||
if (item.role === "user") break
|
||||
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
|
||||
}
|
||||
return result
|
||||
},
|
||||
emptyAssistant,
|
||||
{ equals: same },
|
||||
)
|
||||
|
||||
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
|
||||
|
||||
@@ -131,7 +201,7 @@ export function SessionTurn(
|
||||
const lastTextPart = createMemo(() => {
|
||||
const msgs = assistantMessages()
|
||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? []
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (part?.type === "text") return part as TextPart
|
||||
@@ -151,20 +221,25 @@ export function SessionTurn(
|
||||
return false
|
||||
})
|
||||
|
||||
const permissionParts = createMemo(() => {
|
||||
const result: { part: ToolPart; message: AssistantMessage }[] = []
|
||||
const permissions = data.store.permission?.[props.sessionID] ?? []
|
||||
if (!permissions.length) return result
|
||||
const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions)
|
||||
const permissionCount = createMemo(() => permissions().length)
|
||||
const nextPermission = createMemo(() => permissions()[0])
|
||||
|
||||
for (const m of assistantMessages()) {
|
||||
const msgParts = data.store.part[m.id] ?? []
|
||||
for (const p of msgParts) {
|
||||
if (p?.type === "tool" && permissions.some((perm) => perm.callID === (p as ToolPart).callID)) {
|
||||
result.push({ part: p as ToolPart, message: m })
|
||||
}
|
||||
const permissionParts = createMemo(() => {
|
||||
if (props.stepsExpanded) return emptyPermissionParts
|
||||
|
||||
const next = nextPermission()
|
||||
if (!next) return emptyPermissionParts
|
||||
|
||||
for (const message of assistantMessages()) {
|
||||
const parts = data.store.part[message.id] ?? emptyParts
|
||||
for (const part of parts) {
|
||||
if (part?.type !== "tool") continue
|
||||
const tool = part as ToolPart
|
||||
if (tool.callID === next.callID) return [{ part: tool, message }]
|
||||
}
|
||||
}
|
||||
return result
|
||||
return emptyPermissionParts
|
||||
})
|
||||
|
||||
const shellModePart = createMemo(() => {
|
||||
@@ -174,7 +249,7 @@ export function SessionTurn(
|
||||
const msgs = assistantMessages()
|
||||
if (msgs.length !== 1) return
|
||||
|
||||
const msgParts = data.store.part[msgs[0].id] ?? []
|
||||
const msgParts = data.store.part[msgs[0].id] ?? emptyParts
|
||||
if (msgParts.length !== 1) return
|
||||
|
||||
const assistantPart = msgParts[0]
|
||||
@@ -189,7 +264,7 @@ export function SessionTurn(
|
||||
let currentTask: ToolPart | undefined
|
||||
|
||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? []
|
||||
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (!part) continue
|
||||
@@ -216,12 +291,12 @@ export function SessionTurn(
|
||||
: undefined
|
||||
|
||||
if (taskSessionId) {
|
||||
const taskMessages = data.store.message[taskSessionId] ?? []
|
||||
const taskMessages = data.store.message[taskSessionId] ?? emptyMessages
|
||||
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
|
||||
const msg = taskMessages[mi]
|
||||
if (!msg || msg.role !== "assistant") continue
|
||||
|
||||
const msgParts = data.store.part[msg.id] ?? []
|
||||
const msgParts = data.store.part[msg.id] ?? emptyParts
|
||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||
const part = msgParts[pi]
|
||||
if (part) return computeStatusFromPart(part)
|
||||
@@ -232,12 +307,7 @@ export function SessionTurn(
|
||||
return computeStatusFromPart(last)
|
||||
})
|
||||
|
||||
const status = createMemo(
|
||||
() =>
|
||||
data.store.session_status[props.sessionID] ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
|
||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
@@ -245,12 +315,11 @@ export function SessionTurn(
|
||||
return s
|
||||
})
|
||||
|
||||
const summary = () => message()?.summary?.body
|
||||
const response = () => {
|
||||
const part = lastTextPart()
|
||||
return part?.type === "text" ? (part as TextPart).text : undefined
|
||||
}
|
||||
const hasDiffs = () => message()?.summary?.diffs?.length
|
||||
const summary = createMemo(() => message()?.summary?.body)
|
||||
const response = createMemo(() => lastTextPart()?.text)
|
||||
const responsePartId = createMemo(() => lastTextPart()?.id)
|
||||
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
||||
const hideResponsePart = createMemo(() => !working() && !summary() && !!responsePartId())
|
||||
|
||||
function duration() {
|
||||
const msg = message()
|
||||
@@ -328,6 +397,14 @@ export function SessionTurn(
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(permissionCount, (count, prev) => {
|
||||
if (!count) return
|
||||
if (prev !== undefined && count <= prev) return
|
||||
autoScroll.forceScrollToBottom()
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (working() || !isLastUserMessage()) return
|
||||
|
||||
@@ -336,6 +413,12 @@ export function SessionTurn(
|
||||
if (summary()) return
|
||||
if (store.summaryWaitTimedOut) return
|
||||
|
||||
const completed = lastAssistantMessage()?.time.completed
|
||||
if (completed && Date.now() - completed > 2000) {
|
||||
setStore("summaryWaitTimedOut", true)
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setStore("summaryWaitTimedOut", true)
|
||||
}, 6000)
|
||||
@@ -471,10 +554,8 @@ export function SessionTurn(
|
||||
{(assistantMessage) => (
|
||||
<AssistantMessageItem
|
||||
message={assistantMessage}
|
||||
summary={summary()}
|
||||
response={response()}
|
||||
lastTextPartId={lastTextPart()?.id}
|
||||
working={working()}
|
||||
responsePartId={responsePartId()}
|
||||
hideResponsePart={hideResponsePart()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -205,4 +205,70 @@
|
||||
/* [data-slot="tabs-content"] { */
|
||||
/* } */
|
||||
}
|
||||
|
||||
&[data-orientation="vertical"] {
|
||||
flex-direction: row;
|
||||
|
||||
[data-slot="tabs-list"] {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex-grow: 1;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
|
||||
&:has([data-selected]) {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-content"] {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&[data-variant="alt"] {
|
||||
[data-slot="tabs-list"] {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border-weak-base);
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
border-bottom: none;
|
||||
border-right-width: 2px;
|
||||
border-right-style: solid;
|
||||
border-right-color: transparent;
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
border-right-color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface TabsProps extends ComponentProps<typeof Kobalte> {
|
||||
variant?: "normal" | "alt"
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}
|
||||
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
|
||||
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {
|
||||
@@ -16,12 +17,14 @@ export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger>
|
||||
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||
|
||||
function TabsRoot(props: TabsProps) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "variant"])
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "variant", "orientation"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
orientation={split.orientation}
|
||||
data-component="tabs"
|
||||
data-variant={split.variant || "normal"}
|
||||
data-orientation={split.orientation || "horizontal"}
|
||||
classList={{
|
||||
...(split.classList ?? {}),
|
||||
[split.class ?? ""]: !!split.class,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[data-component="toast-region"] {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
bottom: 48px;
|
||||
right: 32px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
|
||||
@@ -92,7 +92,6 @@ export type ToastVariant = "default" | "success" | "error" | "loading"
|
||||
export interface ToastAction {
|
||||
label: string
|
||||
onClick: "dismiss" | (() => void)
|
||||
dismissAfter?: boolean
|
||||
}
|
||||
|
||||
export interface ToastOptions {
|
||||
@@ -132,10 +131,8 @@ export function showToast(options: ToastOptions | string) {
|
||||
onClick={() => {
|
||||
if (typeof action.onClick === "function") {
|
||||
action.onClick()
|
||||
if (action.dismissAfter) toaster.dismiss(props.toastId)
|
||||
} else {
|
||||
toaster.dismiss(props.toastId)
|
||||
}
|
||||
toaster.dismiss(props.toastId)
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { marked } from "marked"
|
||||
import markedKatex from "marked-katex-extension"
|
||||
import markedShiki from "marked-shiki"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
import { createSimpleContext } from "./helper"
|
||||
@@ -378,6 +379,9 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
|
||||
name: "Marked",
|
||||
init: () => {
|
||||
return marked.use(
|
||||
markedKatex({
|
||||
throwOnError: false,
|
||||
}),
|
||||
markedShiki({
|
||||
async highlight(code, lang) {
|
||||
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
|
||||
|
||||
@@ -19,22 +19,88 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
let autoScrollTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
let isMouseDown = false
|
||||
let cleanupListeners: (() => void) | undefined
|
||||
let scheduledScroll = false
|
||||
let scheduledForce = false
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!scrollRef || store.userScrolled || !options.working()) return
|
||||
function distanceFromBottom() {
|
||||
if (!scrollRef) return 0
|
||||
return scrollRef.scrollHeight - scrollRef.clientHeight - scrollRef.scrollTop
|
||||
}
|
||||
|
||||
function startAutoScroll() {
|
||||
isAutoScrolling = true
|
||||
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
|
||||
autoScrollTimeout = setTimeout(() => {
|
||||
isAutoScrolling = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function scrollToBottomNow() {
|
||||
if (!scrollRef || store.userScrolled || !options.working()) return
|
||||
|
||||
const distance = distanceFromBottom()
|
||||
if (distance < 2) return
|
||||
|
||||
const behavior = distance > 96 ? "auto" : "smooth"
|
||||
startAutoScroll()
|
||||
scrollRef.scrollTo({
|
||||
top: scrollRef.scrollHeight,
|
||||
behavior: "smooth",
|
||||
behavior,
|
||||
})
|
||||
}
|
||||
|
||||
function forceScrollToBottomNow() {
|
||||
if (!scrollRef) return
|
||||
|
||||
if (store.userScrolled) setStore("userScrolled", false)
|
||||
|
||||
const distance = distanceFromBottom()
|
||||
if (distance < 2) return
|
||||
|
||||
startAutoScroll()
|
||||
scrollRef.scrollTo({
|
||||
top: scrollRef.scrollHeight,
|
||||
behavior: "auto",
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleScrollToBottom(force = false) {
|
||||
if (typeof requestAnimationFrame === "undefined") {
|
||||
if (force) {
|
||||
forceScrollToBottomNow()
|
||||
return
|
||||
}
|
||||
scrollToBottomNow()
|
||||
return
|
||||
}
|
||||
|
||||
if (force) scheduledForce = true
|
||||
if (scheduledScroll) return
|
||||
|
||||
scheduledScroll = true
|
||||
requestAnimationFrame(() => {
|
||||
scheduledScroll = false
|
||||
|
||||
const shouldForce = scheduledForce
|
||||
scheduledForce = false
|
||||
|
||||
if (shouldForce) {
|
||||
forceScrollToBottomNow()
|
||||
return
|
||||
}
|
||||
|
||||
scrollToBottomNow()
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
scheduleScrollToBottom(false)
|
||||
}
|
||||
|
||||
function forceScrollToBottom() {
|
||||
scheduleScrollToBottom(true)
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollRef) return
|
||||
|
||||
@@ -116,6 +182,20 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure pinned-to-bottom stays pinned during heavy DOM updates
|
||||
createEffect(() => {
|
||||
const el = store.contentRef
|
||||
if (!el) return
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (store.userScrolled) return
|
||||
if (!options.working()) return
|
||||
scheduleScrollToBottom(false)
|
||||
})
|
||||
observer.observe(el, { childList: true, subtree: true, characterData: true })
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
// Handle content resize
|
||||
createResizeObserver(
|
||||
() => store.contentRef,
|
||||
@@ -161,6 +241,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
||||
handleScroll,
|
||||
handleInteraction,
|
||||
scrollToBottom,
|
||||
forceScrollToBottom,
|
||||
userScrolled: () => store.userScrolled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,17 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
|
||||
)
|
||||
})
|
||||
|
||||
function initialActive() {
|
||||
if (props.current) return props.key(props.current)
|
||||
|
||||
const items = flat()
|
||||
if (items.length === 0) return ""
|
||||
return props.key(items[0])
|
||||
}
|
||||
|
||||
const list = createList({
|
||||
items: () => flat().map(props.key),
|
||||
initialActive: props.current ? props.key(props.current) : props.key(flat()[0]),
|
||||
initialActive: initialActive(),
|
||||
loop: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import "./theme.css" layer(theme);
|
||||
|
||||
@import "./base.css" layer(base);
|
||||
@import "katex/dist/katex.min.css" layer(base);
|
||||
|
||||
@import "../components/accordion.css" layer(components);
|
||||
@import "../components/avatar.css" layer(components);
|
||||
@@ -21,6 +22,7 @@
|
||||
@import "../components/provider-icon.css" layer(components);
|
||||
@import "../components/icon.css" layer(components);
|
||||
@import "../components/icon-button.css" layer(components);
|
||||
@import "../components/image-preview.css" layer(components);
|
||||
@import "../components/text-field.css" layer(components);
|
||||
@import "../components/list.css" layer(components);
|
||||
@import "../components/logo.css" layer(components);
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
color-scheme: light;
|
||||
--text-mix-blend-mode: multiply;
|
||||
|
||||
/* OC-1-light */
|
||||
/* OC-1 fallback variables (light) */
|
||||
--background-base: #f8f7f7;
|
||||
--background-weak: var(--smoke-light-3);
|
||||
--background-strong: var(--smoke-light-1);
|
||||
@@ -324,7 +324,7 @@
|
||||
color-scheme: dark;
|
||||
--text-mix-blend-mode: plus-lighter;
|
||||
|
||||
/* OC-1-dark */
|
||||
/* OC-1 fallback variables (dark) */
|
||||
--background-base: var(--smoke-dark-1);
|
||||
--background-weak: #1c1717;
|
||||
--background-strong: #151313;
|
||||
@@ -577,478 +577,3 @@
|
||||
--avatar-text-lime: #c4f042;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="oc-2-paper"] {
|
||||
/* OC-2-paper */
|
||||
--background-base: #f7f8f8;
|
||||
--background-weak: var(--ink-light-3);
|
||||
--background-strong: var(--ink-light-1);
|
||||
--background-stronger: #fcfcfc;
|
||||
--surface-base: var(--ink-light-alpha-2);
|
||||
--base: var(--ink-light-alpha-2);
|
||||
--surface-base-hover: #0005050f;
|
||||
--surface-base-active: var(--ink-light-alpha-3);
|
||||
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
|
||||
--base2: var(--ink-light-alpha-2);
|
||||
--base3: var(--ink-light-alpha-2);
|
||||
--surface-inset-base: var(--ink-light-alpha-2);
|
||||
--surface-inset-base-hover: var(--ink-light-alpha-3);
|
||||
--surface-inset-strong: #001f1f17;
|
||||
--surface-inset-strong-hover: #001f1f17;
|
||||
--surface-raised-base: var(--ink-light-alpha-1);
|
||||
--surface-float-base: var(--ink-dark-1);
|
||||
--surface-float-base-hover: var(--ink-dark-2);
|
||||
--surface-raised-base-hover: var(--ink-light-alpha-2);
|
||||
--surface-raised-base-active: var(--ink-light-alpha-3);
|
||||
--surface-raised-strong: var(--ink-light-1);
|
||||
--surface-raised-strong-hover: var(--white);
|
||||
--surface-raised-stronger: var(--white);
|
||||
--surface-raised-stronger-hover: var(--white);
|
||||
--surface-weak: var(--ink-light-alpha-3);
|
||||
--surface-weaker: var(--ink-light-alpha-4);
|
||||
--surface-strong: #ffffff;
|
||||
--surface-raised-stronger-non-alpha: var(--white);
|
||||
--surface-brand-base: var(--yuzu-light-9);
|
||||
--surface-brand-hover: var(--yuzu-light-10);
|
||||
--surface-interactive-base: var(--cobalt-light-3);
|
||||
--surface-interactive-hover: var(--cobalt-light-4);
|
||||
--surface-interactive-weak: var(--cobalt-light-2);
|
||||
--surface-interactive-weak-hover: var(--cobalt-light-3);
|
||||
--surface-success-base: var(--apple-light-3);
|
||||
--surface-success-weak: var(--apple-light-2);
|
||||
--surface-success-strong: var(--apple-light-9);
|
||||
--surface-warning-base: var(--solaris-light-3);
|
||||
--surface-warning-weak: var(--solaris-light-2);
|
||||
--surface-warning-strong: var(--solaris-light-9);
|
||||
--surface-critical-base: var(--ember-light-3);
|
||||
--surface-critical-weak: var(--ember-light-2);
|
||||
--surface-critical-strong: var(--ember-light-9);
|
||||
--surface-info-base: var(--lilac-light-3);
|
||||
--surface-info-weak: var(--lilac-light-2);
|
||||
--surface-info-strong: var(--lilac-light-9);
|
||||
--surface-diff-unchanged-base: #ffffff00;
|
||||
--surface-diff-skip-base: var(--ink-light-2);
|
||||
--surface-diff-hidden-base: var(--blue-light-3);
|
||||
--surface-diff-hidden-weak: var(--blue-light-2);
|
||||
--surface-diff-hidden-weaker: var(--blue-light-1);
|
||||
--surface-diff-hidden-strong: var(--blue-light-5);
|
||||
--surface-diff-hidden-stronger: var(--blue-light-9);
|
||||
--surface-diff-add-base: var(--mint-light-3);
|
||||
--surface-diff-add-weak: var(--mint-light-2);
|
||||
--surface-diff-add-weaker: var(--mint-light-1);
|
||||
--surface-diff-add-strong: var(--mint-light-5);
|
||||
--surface-diff-add-stronger: var(--mint-light-9);
|
||||
--surface-diff-delete-base: var(--ember-light-3);
|
||||
--surface-diff-delete-weak: var(--ember-light-2);
|
||||
--surface-diff-delete-weaker: var(--ember-light-1);
|
||||
--surface-diff-delete-strong: var(--ember-light-6);
|
||||
--surface-diff-delete-stronger: var(--ember-light-9);
|
||||
--text-base: var(--ink-light-11);
|
||||
--input-base: var(--ink-light-1);
|
||||
--input-hover: var(--ink-light-2);
|
||||
--input-active: var(--cobalt-light-1);
|
||||
--input-selected: var(--cobalt-light-4);
|
||||
--input-focus: var(--cobalt-light-1);
|
||||
--input-disabled: var(--ink-light-4);
|
||||
--text-weak: var(--ink-light-9);
|
||||
--text-weaker: var(--ink-light-8);
|
||||
--text-strong: var(--ink-light-12);
|
||||
--text-interactive-base: var(--cobalt-light-9);
|
||||
--text-on-brand-base: var(--ink-light-alpha-11);
|
||||
--text-on-interactive-base: var(--ink-light-1);
|
||||
--text-on-interactive-weak: var(--ink-light-alpha-11);
|
||||
--text-on-success-base: var(--apple-light-10);
|
||||
--text-on-critical-base: var(--ember-light-10);
|
||||
--text-on-critical-weak: var(--ember-light-8);
|
||||
--text-on-critical-strong: var(--ember-light-12);
|
||||
--text-on-warning-base: var(--ink-dark-alpha-11);
|
||||
--text-on-info-base: var(--ink-dark-alpha-11);
|
||||
--text-diff-add-base: var(--mint-light-11);
|
||||
--text-diff-delete-base: var(--ember-light-10);
|
||||
--text-diff-delete-strong: var(--ember-light-12);
|
||||
--text-diff-add-strong: var(--mint-light-12);
|
||||
--text-on-info-weak: var(--ink-dark-alpha-9);
|
||||
--text-on-info-strong: var(--ink-dark-alpha-12);
|
||||
--text-on-warning-weak: var(--ink-dark-alpha-9);
|
||||
--text-on-warning-strong: var(--ink-dark-alpha-12);
|
||||
--text-on-success-weak: var(--apple-light-6);
|
||||
--text-on-success-strong: var(--apple-light-12);
|
||||
--text-on-brand-weak: var(--ink-light-alpha-9);
|
||||
--text-on-brand-weaker: var(--ink-light-alpha-8);
|
||||
--text-on-brand-strong: var(--ink-light-alpha-12);
|
||||
--button-secondary-base: #fcfdfd;
|
||||
--button-secondary-hover: #f9fafa;
|
||||
--border-base: var(--ink-light-alpha-7);
|
||||
--border-hover: var(--ink-light-alpha-8);
|
||||
--border-active: var(--ink-light-alpha-9);
|
||||
--border-selected: var(--cobalt-light-alpha-9);
|
||||
--border-disabled: var(--ink-light-alpha-8);
|
||||
--border-focus: var(--ink-light-alpha-9);
|
||||
--border-weak-base: var(--ink-light-alpha-5);
|
||||
--border-strong-base: var(--ink-light-alpha-7);
|
||||
--border-strong-hover: var(--ink-light-alpha-8);
|
||||
--border-strong-active: var(--ink-light-alpha-7);
|
||||
--border-strong-selected: var(--cobalt-light-alpha-6);
|
||||
--border-strong-disabled: var(--ink-light-alpha-6);
|
||||
--border-strong-focus: var(--ink-light-alpha-7);
|
||||
--border-weak-hover: var(--ink-light-alpha-6);
|
||||
--border-weak-active: var(--ink-light-alpha-7);
|
||||
--border-weak-selected: var(--cobalt-light-alpha-5);
|
||||
--border-weak-disabled: var(--ink-light-alpha-6);
|
||||
--border-weak-focus: var(--ink-light-alpha-7);
|
||||
--border-interactive-base: var(--cobalt-light-7);
|
||||
--border-interactive-hover: var(--cobalt-light-8);
|
||||
--border-interactive-active: var(--cobalt-light-9);
|
||||
--border-interactive-selected: var(--cobalt-light-9);
|
||||
--border-interactive-disabled: var(--ink-light-8);
|
||||
--border-interactive-focus: var(--cobalt-light-9);
|
||||
--border-success-base: var(--apple-light-6);
|
||||
--border-success-hover: var(--apple-light-7);
|
||||
--border-success-selected: var(--apple-light-9);
|
||||
--border-warning-base: var(--solaris-light-6);
|
||||
--border-warning-hover: var(--solaris-light-7);
|
||||
--border-warning-selected: var(--solaris-light-9);
|
||||
--border-critical-base: var(--ember-light-6);
|
||||
--border-critical-hover: var(--ember-light-7);
|
||||
--border-critical-selected: var(--ember-light-9);
|
||||
--border-info-base: var(--lilac-light-6);
|
||||
--border-info-hover: var(--lilac-light-7);
|
||||
--border-info-selected: var(--lilac-light-9);
|
||||
--icon-base: var(--ink-light-9);
|
||||
--icon-hover: var(--ink-light-11);
|
||||
--icon-active: var(--ink-light-12);
|
||||
--icon-selected: var(--ink-light-12);
|
||||
--icon-disabled: var(--ink-light-8);
|
||||
--icon-focus: var(--ink-light-12);
|
||||
--icon-invert-base: #ffffff;
|
||||
--icon-weak-base: var(--ink-light-7);
|
||||
--icon-weak-hover: var(--ink-light-8);
|
||||
--icon-weak-active: var(--ink-light-9);
|
||||
--icon-weak-selected: var(--ink-light-10);
|
||||
--icon-weak-disabled: var(--ink-light-6);
|
||||
--icon-weak-focus: var(--ink-light-9);
|
||||
--icon-strong-base: var(--ink-light-12);
|
||||
--icon-strong-hover: #131515;
|
||||
--icon-strong-active: #020202;
|
||||
--icon-strong-selected: #020202;
|
||||
--icon-strong-disabled: var(--ink-light-8);
|
||||
--icon-strong-focus: #020202;
|
||||
--icon-brand-base: var(--ink-light-12);
|
||||
--icon-interactive-base: var(--cobalt-light-9);
|
||||
--icon-success-base: var(--apple-light-7);
|
||||
--icon-success-hover: var(--apple-light-8);
|
||||
--icon-success-active: var(--apple-light-11);
|
||||
--icon-warning-base: var(--amber-light-7);
|
||||
--icon-warning-hover: var(--amber-light-8);
|
||||
--icon-warning-active: var(--amber-light-11);
|
||||
--icon-critical-base: var(--ember-light-10);
|
||||
--icon-critical-hover: var(--ember-light-11);
|
||||
--icon-critical-active: var(--ember-light-12);
|
||||
--icon-info-base: var(--lilac-light-7);
|
||||
--icon-info-hover: var(--lilac-light-8);
|
||||
--icon-info-active: var(--lilac-light-11);
|
||||
--icon-on-brand-base: var(--ink-light-alpha-11);
|
||||
--icon-on-brand-hover: var(--ink-light-alpha-12);
|
||||
--icon-on-brand-selected: var(--ink-light-alpha-12);
|
||||
--icon-on-interactive-base: var(--ink-light-1);
|
||||
--icon-agent-plan-base: var(--purple-light-9);
|
||||
--icon-agent-docs-base: var(--amber-light-9);
|
||||
--icon-agent-ask-base: var(--cyan-light-9);
|
||||
--icon-agent-build-base: var(--cobalt-light-9);
|
||||
--icon-on-success-base: var(--apple-light-alpha-9);
|
||||
--icon-on-success-hover: var(--apple-light-alpha-10);
|
||||
--icon-on-success-selected: var(--apple-light-alpha-11);
|
||||
--icon-on-warning-base: var(--amber-lightalpha-9);
|
||||
--icon-on-warning-hover: var(--amber-lightalpha-10);
|
||||
--icon-on-warning-selected: var(--amber-lightalpha-11);
|
||||
--icon-on-critical-base: var(--ember-light-alpha-9);
|
||||
--icon-on-critical-hover: var(--ember-light-alpha-10);
|
||||
--icon-on-critical-selected: var(--ember-light-alpha-11);
|
||||
--icon-on-info-base: var(--lilac-light-9);
|
||||
--icon-on-info-hover: var(--lilac-light-alpha-10);
|
||||
--icon-on-info-selected: var(--lilac-light-alpha-11);
|
||||
--icon-diff-add-base: var(--mint-light-11);
|
||||
--icon-diff-add-hover: var(--mint-light-12);
|
||||
--icon-diff-add-active: var(--mint-light-12);
|
||||
--icon-diff-delete-base: var(--ember-light-10);
|
||||
--icon-diff-delete-hover: var(--ember-light-11);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #007663;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
--syntax-primitive: #fb7f51;
|
||||
--syntax-operator: var(--text-weak);
|
||||
--syntax-variable: var(--text-strong);
|
||||
--syntax-property: #ec6cc8;
|
||||
--syntax-type: #738400;
|
||||
--syntax-constant: #00b2b9;
|
||||
--syntax-punctuation: var(--text-weak);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-light-10);
|
||||
--syntax-warning: var(--amber-light-10);
|
||||
--syntax-critical: var(--ember-light-9);
|
||||
--syntax-info: #0091a7;
|
||||
--syntax-diff-add: var(--mint-light-11);
|
||||
--syntax-diff-delete: var(--ember-light-11);
|
||||
--syntax-diff-unknown: #ff0000;
|
||||
--markdown-heading: #d68c27;
|
||||
--markdown-text: #1a1a1a;
|
||||
--markdown-link: #3b7dd8;
|
||||
--markdown-link-text: #318795;
|
||||
--markdown-code: #3d9a57;
|
||||
--markdown-block-quote: #b0851f;
|
||||
--markdown-emph: #b0851f;
|
||||
--markdown-strong: #d68c27;
|
||||
--markdown-horizontal-rule: #8a8a8a;
|
||||
--markdown-list-item: #3b7dd8;
|
||||
--markdown-list-enumeration: #318795;
|
||||
--markdown-image: #3b7dd8;
|
||||
--markdown-image-text: #318795;
|
||||
--markdown-code-block: #1a1a1a;
|
||||
--border-color: #ffffff;
|
||||
--border-weaker-base: var(--ink-light-alpha-3);
|
||||
--border-weaker-hover: var(--ink-light-alpha-4);
|
||||
--border-weaker-active: var(--ink-light-alpha-6);
|
||||
--border-weaker-selected: var(--cobalt-light-alpha-4);
|
||||
--border-weaker-disabled: var(--ink-light-alpha-2);
|
||||
--border-weaker-focus: var(--ink-light-alpha-6);
|
||||
--button-ghost-hover: var(--ink-light-alpha-2);
|
||||
--button-ghost-hover2: var(--ink-light-alpha-3);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--background-base: var(--ink-dark-1);
|
||||
--background-weak: #171c1c;
|
||||
--background-strong: #131515;
|
||||
--background-stronger: #151919;
|
||||
--surface-base: var(--ink-dark-alpha-2);
|
||||
--base: var(--ink-dark-alpha-2);
|
||||
--surface-base-hover: #b8e0e00f;
|
||||
--surface-base-active: var(--ink-dark-alpha-3);
|
||||
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
|
||||
--base2: var(--ink-dark-alpha-2);
|
||||
--base3: var(--ink-dark-alpha-2);
|
||||
--surface-inset-base: #0b0e0e7f;
|
||||
--surface-inset-base-hover: #0b0e0e7f;
|
||||
--surface-inset-strong: #050606cc;
|
||||
--surface-inset-strong-hover: #050606cc;
|
||||
--surface-raised-base: var(--ink-light-alpha-1);
|
||||
--surface-float-base: var(--ink-dark-1);
|
||||
--surface-float-base-hover: var(--ink-dark-2);
|
||||
--surface-raised-base-hover: var(--ink-light-alpha-2);
|
||||
--surface-raised-base-active: var(--ink-light-alpha-3);
|
||||
--surface-raised-strong: var(--ink-light-1);
|
||||
--surface-raised-strong-hover: var(--white);
|
||||
--surface-raised-stronger: var(--white);
|
||||
--surface-raised-stronger-hover: var(--white);
|
||||
--surface-weak: var(--ink-dark-alpha-4);
|
||||
--surface-weaker: var(--ink-dark-alpha-5);
|
||||
--surface-strong: var(--ink-dark-alpha-7);
|
||||
--surface-raised-stronger-non-alpha: var(--white);
|
||||
--surface-brand-base: var(--yuzu-light-9);
|
||||
--surface-brand-hover: var(--yuzu-light-10);
|
||||
--surface-interactive-base: var(--cobalt-light-3);
|
||||
--surface-interactive-hover: var(--cobalt-light-4);
|
||||
--surface-interactive-weak: var(--cobalt-light-2);
|
||||
--surface-interactive-weak-hover: var(--cobalt-light-3);
|
||||
--surface-success-base: var(--apple-light-3);
|
||||
--surface-success-weak: var(--apple-light-2);
|
||||
--surface-success-strong: var(--apple-light-9);
|
||||
--surface-warning-base: var(--solaris-light-3);
|
||||
--surface-warning-weak: var(--solaris-light-2);
|
||||
--surface-warning-strong: var(--solaris-light-9);
|
||||
--surface-critical-base: var(--ember-light-3);
|
||||
--surface-critical-weak: var(--ember-light-2);
|
||||
--surface-critical-strong: var(--ember-light-9);
|
||||
--surface-info-base: var(--lilac-light-3);
|
||||
--surface-info-weak: var(--lilac-light-2);
|
||||
--surface-info-strong: var(--lilac-light-9);
|
||||
--surface-diff-unchanged-base: #ffffff00;
|
||||
--surface-diff-skip-base: var(--ink-light-2);
|
||||
--surface-diff-hidden-base: var(--blue-light-3);
|
||||
--surface-diff-hidden-weak: var(--blue-light-2);
|
||||
--surface-diff-hidden-weaker: var(--blue-light-1);
|
||||
--surface-diff-hidden-strong: var(--blue-light-5);
|
||||
--surface-diff-hidden-stronger: var(--blue-light-9);
|
||||
--surface-diff-add-base: var(--mint-light-3);
|
||||
--surface-diff-add-weak: var(--mint-light-2);
|
||||
--surface-diff-add-weaker: var(--mint-light-1);
|
||||
--surface-diff-add-strong: var(--mint-light-5);
|
||||
--surface-diff-add-stronger: var(--mint-light-9);
|
||||
--surface-diff-delete-base: var(--ember-light-3);
|
||||
--surface-diff-delete-weak: var(--ember-light-2);
|
||||
--surface-diff-delete-weaker: var(--ember-light-1);
|
||||
--surface-diff-delete-strong: var(--ember-light-6);
|
||||
--surface-diff-delete-stronger: var(--ember-light-9);
|
||||
--text-base: var(--ink-light-11);
|
||||
--input-base: var(--ink-light-1);
|
||||
--input-hover: var(--ink-light-2);
|
||||
--input-active: var(--cobalt-light-1);
|
||||
--input-selected: var(--cobalt-light-4);
|
||||
--input-focus: var(--cobalt-light-1);
|
||||
--input-disabled: var(--ink-light-4);
|
||||
--text-weak: var(--ink-light-9);
|
||||
--text-weaker: var(--ink-light-8);
|
||||
--text-strong: var(--ink-light-12);
|
||||
--text-interactive-base: var(--cobalt-light-9);
|
||||
--text-on-brand-base: var(--ink-light-alpha-11);
|
||||
--text-on-interactive-base: var(--ink-light-1);
|
||||
--text-on-interactive-weak: var(--ink-light-alpha-11);
|
||||
--text-on-success-base: var(--apple-light-10);
|
||||
--text-on-critical-base: var(--ember-light-10);
|
||||
--text-on-critical-weak: var(--ember-light-8);
|
||||
--text-on-critical-strong: var(--ember-light-12);
|
||||
--text-on-warning-base: var(--ink-dark-alpha-11);
|
||||
--text-on-info-base: var(--ink-dark-alpha-11);
|
||||
--text-diff-add-base: var(--mint-light-11);
|
||||
--text-diff-delete-base: var(--ember-light-10);
|
||||
--text-diff-delete-strong: var(--ember-light-12);
|
||||
--text-diff-add-strong: var(--mint-light-12);
|
||||
--text-on-info-weak: var(--ink-dark-alpha-9);
|
||||
--text-on-info-strong: var(--ink-dark-alpha-12);
|
||||
--text-on-warning-weak: var(--ink-dark-alpha-9);
|
||||
--text-on-warning-strong: var(--ink-dark-alpha-12);
|
||||
--text-on-success-weak: var(--apple-light-6);
|
||||
--text-on-success-strong: var(--apple-light-12);
|
||||
--text-on-brand-weak: var(--ink-light-alpha-9);
|
||||
--text-on-brand-weaker: var(--ink-light-alpha-8);
|
||||
--text-on-brand-strong: var(--ink-light-alpha-12);
|
||||
--button-secondary-base: #fcfdfd;
|
||||
--button-secondary-hover: #f9fafa;
|
||||
--border-base: var(--ink-light-alpha-7);
|
||||
--border-hover: var(--ink-light-alpha-8);
|
||||
--border-active: var(--ink-light-alpha-9);
|
||||
--border-selected: var(--cobalt-light-alpha-9);
|
||||
--border-disabled: var(--ink-light-alpha-8);
|
||||
--border-focus: var(--ink-light-alpha-9);
|
||||
--border-weak-base: var(--ink-light-alpha-5);
|
||||
--border-strong-base: var(--ink-light-alpha-7);
|
||||
--border-strong-hover: var(--ink-light-alpha-8);
|
||||
--border-strong-active: var(--ink-light-alpha-7);
|
||||
--border-strong-selected: var(--cobalt-light-alpha-6);
|
||||
--border-strong-disabled: var(--ink-light-alpha-6);
|
||||
--border-strong-focus: var(--ink-light-alpha-7);
|
||||
--border-weak-hover: var(--ink-light-alpha-6);
|
||||
--border-weak-active: var(--ink-light-alpha-7);
|
||||
--border-weak-selected: var(--cobalt-light-alpha-5);
|
||||
--border-weak-disabled: var(--ink-light-alpha-6);
|
||||
--border-weak-focus: var(--ink-light-alpha-7);
|
||||
--border-interactive-base: var(--cobalt-light-7);
|
||||
--border-interactive-hover: var(--cobalt-light-8);
|
||||
--border-interactive-active: var(--cobalt-light-9);
|
||||
--border-interactive-selected: var(--cobalt-light-9);
|
||||
--border-interactive-disabled: var(--ink-light-8);
|
||||
--border-interactive-focus: var(--cobalt-light-9);
|
||||
--border-success-base: var(--apple-light-6);
|
||||
--border-success-hover: var(--apple-light-7);
|
||||
--border-success-selected: var(--apple-light-9);
|
||||
--border-warning-base: var(--solaris-light-6);
|
||||
--border-warning-hover: var(--solaris-light-7);
|
||||
--border-warning-selected: var(--solaris-light-9);
|
||||
--border-critical-base: var(--ember-light-6);
|
||||
--border-critical-hover: var(--ember-light-7);
|
||||
--border-critical-selected: var(--ember-light-9);
|
||||
--border-info-base: var(--lilac-light-6);
|
||||
--border-info-hover: var(--lilac-light-7);
|
||||
--border-info-selected: var(--lilac-light-9);
|
||||
--icon-base: var(--ink-light-9);
|
||||
--icon-hover: var(--ink-light-11);
|
||||
--icon-active: var(--ink-light-12);
|
||||
--icon-selected: var(--ink-light-12);
|
||||
--icon-disabled: var(--ink-light-8);
|
||||
--icon-focus: var(--ink-light-12);
|
||||
--icon-invert-base: #ffffff;
|
||||
--icon-weak-base: var(--ink-light-7);
|
||||
--icon-weak-hover: var(--ink-light-8);
|
||||
--icon-weak-active: var(--ink-light-9);
|
||||
--icon-weak-selected: var(--ink-light-10);
|
||||
--icon-weak-disabled: var(--ink-light-6);
|
||||
--icon-weak-focus: var(--ink-light-9);
|
||||
--icon-strong-base: var(--ink-light-12);
|
||||
--icon-strong-hover: #131515;
|
||||
--icon-strong-active: #020202;
|
||||
--icon-strong-selected: #020202;
|
||||
--icon-strong-disabled: var(--ink-light-8);
|
||||
--icon-strong-focus: #020202;
|
||||
--icon-brand-base: var(--ink-light-12);
|
||||
--icon-interactive-base: var(--cobalt-light-9);
|
||||
--icon-success-base: var(--apple-light-7);
|
||||
--icon-success-hover: var(--apple-light-8);
|
||||
--icon-success-active: var(--apple-light-11);
|
||||
--icon-warning-base: var(--amber-light-7);
|
||||
--icon-warning-hover: var(--amber-light-8);
|
||||
--icon-warning-active: var(--amber-light-11);
|
||||
--icon-critical-base: var(--ember-light-10);
|
||||
--icon-critical-hover: var(--ember-light-11);
|
||||
--icon-critical-active: var(--ember-light-12);
|
||||
--icon-info-base: var(--lilac-light-7);
|
||||
--icon-info-hover: var(--lilac-light-8);
|
||||
--icon-info-active: var(--lilac-light-11);
|
||||
--icon-on-brand-base: var(--ink-light-alpha-11);
|
||||
--icon-on-brand-hover: var(--ink-light-alpha-12);
|
||||
--icon-on-brand-selected: var(--ink-light-alpha-12);
|
||||
--icon-on-interactive-base: var(--ink-light-1);
|
||||
--icon-agent-plan-base: var(--purple-light-9);
|
||||
--icon-agent-docs-base: var(--amber-light-9);
|
||||
--icon-agent-ask-base: var(--cyan-light-9);
|
||||
--icon-agent-build-base: var(--cobalt-light-9);
|
||||
--icon-on-success-base: var(--apple-light-alpha-9);
|
||||
--icon-on-success-hover: var(--apple-light-alpha-10);
|
||||
--icon-on-success-selected: var(--apple-light-alpha-11);
|
||||
--icon-on-warning-base: var(--amber-lightalpha-9);
|
||||
--icon-on-warning-hover: var(--amber-lightalpha-10);
|
||||
--icon-on-warning-selected: var(--amber-lightalpha-11);
|
||||
--icon-on-critical-base: var(--ember-light-alpha-9);
|
||||
--icon-on-critical-hover: var(--ember-light-alpha-10);
|
||||
--icon-on-critical-selected: var(--ember-light-alpha-11);
|
||||
--icon-on-info-base: var(--lilac-light-9);
|
||||
--icon-on-info-hover: var(--lilac-light-alpha-10);
|
||||
--icon-on-info-selected: var(--lilac-light-alpha-11);
|
||||
--icon-diff-add-base: var(--mint-light-11);
|
||||
--icon-diff-add-hover: var(--mint-light-12);
|
||||
--icon-diff-add-active: var(--mint-light-12);
|
||||
--icon-diff-delete-base: var(--ember-light-10);
|
||||
--icon-diff-delete-hover: var(--ember-light-11);
|
||||
--syntax-comment: var(--text-weak);
|
||||
--syntax-regexp: var(--text-base);
|
||||
--syntax-string: #007663;
|
||||
--syntax-keyword: var(--text-weak);
|
||||
--syntax-primitive: #fb7f51;
|
||||
--syntax-operator: var(--text-weak);
|
||||
--syntax-variable: var(--text-strong);
|
||||
--syntax-property: #ec6cc8;
|
||||
--syntax-type: #738400;
|
||||
--syntax-constant: #00b2b9;
|
||||
--syntax-punctuation: var(--text-weak);
|
||||
--syntax-object: var(--text-strong);
|
||||
--syntax-success: var(--apple-light-10);
|
||||
--syntax-warning: var(--amber-light-10);
|
||||
--syntax-critical: var(--ember-light-9);
|
||||
--syntax-info: #0091a7;
|
||||
--syntax-diff-add: var(--mint-light-11);
|
||||
--syntax-diff-delete: var(--ember-light-11);
|
||||
--syntax-diff-unknown: #ff0000;
|
||||
--markdown-heading: #d68c27;
|
||||
--markdown-text: #1a1a1a;
|
||||
--markdown-link: #3b7dd8;
|
||||
--markdown-link-text: #318795;
|
||||
--markdown-code: #3d9a57;
|
||||
--markdown-block-quote: #b0851f;
|
||||
--markdown-emph: #b0851f;
|
||||
--markdown-strong: #d68c27;
|
||||
--markdown-horizontal-rule: #8a8a8a;
|
||||
--markdown-list-item: #3b7dd8;
|
||||
--markdown-list-enumeration: #318795;
|
||||
--markdown-image: #3b7dd8;
|
||||
--markdown-image-text: #318795;
|
||||
--markdown-code-block: #1a1a1a;
|
||||
--border-color: #ffffff;
|
||||
--border-weaker-base: var(--ink-light-alpha-3);
|
||||
--border-weaker-hover: var(--ink-light-alpha-4);
|
||||
--border-weaker-active: var(--ink-light-alpha-6);
|
||||
--border-weaker-selected: var(--cobalt-light-alpha-4);
|
||||
--border-weaker-disabled: var(--ink-light-alpha-2);
|
||||
--border-weaker-focus: var(--ink-light-alpha-6);
|
||||
--button-ghost-hover: var(--ink-light-alpha-2);
|
||||
--button-ghost-hover2: var(--ink-light-alpha-3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { HexColor, OklchColor } from "./types"
|
||||
|
||||
export function hexToRgb(hex: HexColor): { r: number; g: number; b: number } {
|
||||
const h = hex.replace("#", "")
|
||||
const full =
|
||||
h.length === 3
|
||||
? h
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: h
|
||||
|
||||
const num = parseInt(full, 16)
|
||||
return {
|
||||
r: ((num >> 16) & 255) / 255,
|
||||
g: ((num >> 8) & 255) / 255,
|
||||
b: (num & 255) / 255,
|
||||
}
|
||||
}
|
||||
|
||||
export function rgbToHex(r: number, g: number, b: number): HexColor {
|
||||
const toHex = (v: number) => {
|
||||
const clamped = Math.max(0, Math.min(1, v))
|
||||
const int = Math.round(clamped * 255)
|
||||
return int.toString(16).padStart(2, "0")
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function linearToSrgb(c: number): number {
|
||||
if (c <= 0.0031308) return c * 12.92
|
||||
return 1.055 * Math.pow(c, 1 / 2.4) - 0.055
|
||||
}
|
||||
|
||||
function srgbToLinear(c: number): number {
|
||||
if (c <= 0.04045) return c / 12.92
|
||||
return Math.pow((c + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
export function rgbToOklch(r: number, g: number, b: number): OklchColor {
|
||||
const lr = srgbToLinear(r)
|
||||
const lg = srgbToLinear(g)
|
||||
const lb = srgbToLinear(b)
|
||||
|
||||
const l_ = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
|
||||
const m_ = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
|
||||
const s_ = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
|
||||
|
||||
const l = Math.cbrt(l_)
|
||||
const m = Math.cbrt(m_)
|
||||
const s = Math.cbrt(s_)
|
||||
|
||||
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s
|
||||
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s
|
||||
const bOk = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
||||
|
||||
const C = Math.sqrt(a * a + bOk * bOk)
|
||||
let H = Math.atan2(bOk, a) * (180 / Math.PI)
|
||||
if (H < 0) H += 360
|
||||
|
||||
return { l: L, c: C, h: H }
|
||||
}
|
||||
|
||||
export function oklchToRgb(oklch: OklchColor): { r: number; g: number; b: number } {
|
||||
const { l: L, c: C, h: H } = oklch
|
||||
|
||||
const a = C * Math.cos((H * Math.PI) / 180)
|
||||
const b = C * Math.sin((H * Math.PI) / 180)
|
||||
|
||||
const l = L + 0.3963377774 * a + 0.2158037573 * b
|
||||
const m = L - 0.1055613458 * a - 0.0638541728 * b
|
||||
const s = L - 0.0894841775 * a - 1.291485548 * b
|
||||
|
||||
const l3 = l * l * l
|
||||
const m3 = m * m * m
|
||||
const s3 = s * s * s
|
||||
|
||||
const lr = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3
|
||||
const lg = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3
|
||||
const lb = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3
|
||||
|
||||
return {
|
||||
r: linearToSrgb(lr),
|
||||
g: linearToSrgb(lg),
|
||||
b: linearToSrgb(lb),
|
||||
}
|
||||
}
|
||||
|
||||
export function hexToOklch(hex: HexColor): OklchColor {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
return rgbToOklch(r, g, b)
|
||||
}
|
||||
|
||||
export function oklchToHex(oklch: OklchColor): HexColor {
|
||||
const { r, g, b } = oklchToRgb(oklch)
|
||||
return rgbToHex(r, g, b)
|
||||
}
|
||||
|
||||
export function generateScale(seed: HexColor, isDark: boolean): HexColor[] {
|
||||
const base = hexToOklch(seed)
|
||||
const scale: HexColor[] = []
|
||||
|
||||
const lightSteps = isDark
|
||||
? [0.15, 0.18, 0.22, 0.26, 0.32, 0.38, 0.46, 0.56, base.l, base.l - 0.05, 0.75, 0.93]
|
||||
: [0.99, 0.97, 0.94, 0.9, 0.85, 0.79, 0.72, 0.64, base.l, base.l + 0.05, 0.45, 0.25]
|
||||
|
||||
const chromaMultipliers = isDark
|
||||
? [0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.85, 1, 1, 0.9, 0.6]
|
||||
: [0.1, 0.15, 0.25, 0.35, 0.45, 0.55, 0.7, 0.85, 1, 1, 0.95, 0.85]
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
scale.push(
|
||||
oklchToHex({
|
||||
l: lightSteps[i],
|
||||
c: base.c * chromaMultipliers[i],
|
||||
h: base.h,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return scale
|
||||
}
|
||||
|
||||
export function generateNeutralScale(seed: HexColor, isDark: boolean): HexColor[] {
|
||||
const base = hexToOklch(seed)
|
||||
const scale: HexColor[] = []
|
||||
const neutralChroma = Math.min(base.c, 0.02)
|
||||
|
||||
const lightSteps = isDark
|
||||
? [0.13, 0.16, 0.2, 0.24, 0.28, 0.33, 0.4, 0.52, 0.58, 0.66, 0.82, 0.96]
|
||||
: [0.995, 0.98, 0.96, 0.94, 0.91, 0.88, 0.84, 0.78, 0.62, 0.56, 0.46, 0.2]
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
scale.push(
|
||||
oklchToHex({
|
||||
l: lightSteps[i],
|
||||
c: neutralChroma,
|
||||
h: base.h,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return scale
|
||||
}
|
||||
|
||||
export function generateAlphaScale(scale: HexColor[], isDark: boolean): HexColor[] {
|
||||
const alphas = isDark
|
||||
? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.76, 0.96]
|
||||
: [0.01, 0.03, 0.06, 0.09, 0.12, 0.15, 0.2, 0.28, 0.48, 0.56, 0.64, 0.88]
|
||||
|
||||
return scale.map((hex, i) => {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
const a = alphas[i]
|
||||
|
||||
const bg = isDark ? 0 : 1
|
||||
const blendedR = r * a + bg * (1 - a)
|
||||
const blendedG = g * a + bg * (1 - a)
|
||||
const blendedB = b * a + bg * (1 - a)
|
||||
|
||||
return rgbToHex(blendedR, blendedG, blendedB)
|
||||
})
|
||||
}
|
||||
|
||||
export function mixColors(color1: HexColor, color2: HexColor, amount: number): HexColor {
|
||||
const c1 = hexToOklch(color1)
|
||||
const c2 = hexToOklch(color2)
|
||||
|
||||
return oklchToHex({
|
||||
l: c1.l + (c2.l - c1.l) * amount,
|
||||
c: c1.c + (c2.c - c1.c) * amount,
|
||||
h: c1.h + (c2.h - c1.h) * amount,
|
||||
})
|
||||
}
|
||||
|
||||
export function lighten(color: HexColor, amount: number): HexColor {
|
||||
const oklch = hexToOklch(color)
|
||||
return oklchToHex({
|
||||
...oklch,
|
||||
l: Math.min(1, oklch.l + amount),
|
||||
})
|
||||
}
|
||||
|
||||
export function darken(color: HexColor, amount: number): HexColor {
|
||||
const oklch = hexToOklch(color)
|
||||
return oklchToHex({
|
||||
...oklch,
|
||||
l: Math.max(0, oklch.l - amount),
|
||||
})
|
||||
}
|
||||
|
||||
export function withAlpha(color: HexColor, alpha: number): string {
|
||||
const { r, g, b } = hexToRgb(color)
|
||||
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { onMount, onCleanup, createEffect } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { DesktopTheme } from "./types"
|
||||
import { resolveThemeVariant, themeToCss } from "./resolve"
|
||||
import { DEFAULT_THEMES } from "./default-themes"
|
||||
import { createSimpleContext } from "../context/helper"
|
||||
|
||||
export type ColorScheme = "light" | "dark" | "system"
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
THEME_ID: "opencode-theme-id",
|
||||
COLOR_SCHEME: "opencode-color-scheme",
|
||||
THEME_CSS_LIGHT: "opencode-theme-css-light",
|
||||
THEME_CSS_DARK: "opencode-theme-css-dark",
|
||||
} as const
|
||||
|
||||
const THEME_STYLE_ID = "oc-theme"
|
||||
|
||||
function ensureThemeStyleElement(): HTMLStyleElement {
|
||||
const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
|
||||
if (existing) return existing
|
||||
const element = document.createElement("style")
|
||||
element.id = THEME_STYLE_ID
|
||||
document.head.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
function getSystemMode(): "light" | "dark" {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
}
|
||||
|
||||
function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "dark") {
|
||||
const isDark = mode === "dark"
|
||||
const variant = isDark ? theme.dark : theme.light
|
||||
const tokens = resolveThemeVariant(variant, isDark)
|
||||
const css = themeToCss(tokens)
|
||||
|
||||
if (themeId !== "oc-1") {
|
||||
try {
|
||||
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const fullCss = `:root {
|
||||
color-scheme: ${mode};
|
||||
--text-mix-blend-mode: ${isDark ? "plus-lighter" : "multiply"};
|
||||
${css}
|
||||
}`
|
||||
|
||||
document.getElementById("oc-theme-preload")?.remove()
|
||||
ensureThemeStyleElement().textContent = fullCss
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
}
|
||||
|
||||
function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
|
||||
if (themeId === "oc-1") return
|
||||
for (const mode of ["light", "dark"] as const) {
|
||||
const isDark = mode === "dark"
|
||||
const variant = isDark ? theme.dark : theme.light
|
||||
const tokens = resolveThemeVariant(variant, isDark)
|
||||
const css = themeToCss(tokens)
|
||||
try {
|
||||
localStorage.setItem(isDark ? STORAGE_KEYS.THEME_CSS_DARK : STORAGE_KEYS.THEME_CSS_LIGHT, css)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: (props: { defaultTheme?: string }) => {
|
||||
const [store, setStore] = createStore({
|
||||
themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
|
||||
themeId: props.defaultTheme ?? "oc-1",
|
||||
colorScheme: "system" as ColorScheme,
|
||||
mode: getSystemMode(),
|
||||
previewThemeId: null as string | null,
|
||||
previewScheme: null as ColorScheme | null,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const handler = () => {
|
||||
if (store.colorScheme === "system") {
|
||||
setStore("mode", getSystemMode())
|
||||
}
|
||||
}
|
||||
mediaQuery.addEventListener("change", handler)
|
||||
onCleanup(() => mediaQuery.removeEventListener("change", handler))
|
||||
|
||||
const savedTheme = localStorage.getItem(STORAGE_KEYS.THEME_ID)
|
||||
const savedScheme = localStorage.getItem(STORAGE_KEYS.COLOR_SCHEME) as ColorScheme | null
|
||||
if (savedTheme && store.themes[savedTheme]) {
|
||||
setStore("themeId", savedTheme)
|
||||
}
|
||||
if (savedScheme) {
|
||||
setStore("colorScheme", savedScheme)
|
||||
if (savedScheme !== "system") {
|
||||
setStore("mode", savedScheme)
|
||||
}
|
||||
}
|
||||
const currentTheme = store.themes[store.themeId]
|
||||
if (currentTheme) {
|
||||
cacheThemeVariants(currentTheme, store.themeId)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const theme = store.themes[store.themeId]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, store.themeId, store.mode)
|
||||
}
|
||||
})
|
||||
|
||||
const setTheme = (id: string) => {
|
||||
const theme = store.themes[id]
|
||||
if (!theme) {
|
||||
console.warn(`Theme "${id}" not found`)
|
||||
return
|
||||
}
|
||||
setStore("themeId", id)
|
||||
localStorage.setItem(STORAGE_KEYS.THEME_ID, id)
|
||||
cacheThemeVariants(theme, id)
|
||||
}
|
||||
|
||||
const setColorScheme = (scheme: ColorScheme) => {
|
||||
setStore("colorScheme", scheme)
|
||||
localStorage.setItem(STORAGE_KEYS.COLOR_SCHEME, scheme)
|
||||
setStore("mode", scheme === "system" ? getSystemMode() : scheme)
|
||||
}
|
||||
|
||||
return {
|
||||
themeId: () => store.themeId,
|
||||
colorScheme: () => store.colorScheme,
|
||||
mode: () => store.mode,
|
||||
themes: () => store.themes,
|
||||
setTheme,
|
||||
setColorScheme,
|
||||
registerTheme: (theme: DesktopTheme) => setStore("themes", theme.id, theme),
|
||||
previewTheme: (id: string) => {
|
||||
const theme = store.themes[id]
|
||||
if (!theme) return
|
||||
setStore("previewThemeId", id)
|
||||
const previewMode = store.previewScheme
|
||||
? store.previewScheme === "system"
|
||||
? getSystemMode()
|
||||
: store.previewScheme
|
||||
: store.mode
|
||||
applyThemeCss(theme, id, previewMode)
|
||||
},
|
||||
previewColorScheme: (scheme: ColorScheme) => {
|
||||
setStore("previewScheme", scheme)
|
||||
const previewMode = scheme === "system" ? getSystemMode() : scheme
|
||||
const id = store.previewThemeId ?? store.themeId
|
||||
const theme = store.themes[id]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, id, previewMode)
|
||||
}
|
||||
},
|
||||
commitPreview: () => {
|
||||
if (store.previewThemeId) {
|
||||
setTheme(store.previewThemeId)
|
||||
}
|
||||
if (store.previewScheme) {
|
||||
setColorScheme(store.previewScheme)
|
||||
}
|
||||
setStore("previewThemeId", null)
|
||||
setStore("previewScheme", null)
|
||||
},
|
||||
cancelPreview: () => {
|
||||
setStore("previewThemeId", null)
|
||||
setStore("previewScheme", null)
|
||||
const theme = store.themes[store.themeId]
|
||||
if (theme) {
|
||||
applyThemeCss(theme, store.themeId, store.mode)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { DesktopTheme } from "./types"
|
||||
import oc1ThemeJson from "./themes/oc-1.json"
|
||||
import tokyoThemeJson from "./themes/tokyonight.json"
|
||||
import draculaThemeJson from "./themes/dracula.json"
|
||||
import monokaiThemeJson from "./themes/monokai.json"
|
||||
import solarizedThemeJson from "./themes/solarized.json"
|
||||
import nordThemeJson from "./themes/nord.json"
|
||||
import catppuccinThemeJson from "./themes/catppuccin.json"
|
||||
import ayuThemeJson from "./themes/ayu.json"
|
||||
import oneDarkProThemeJson from "./themes/onedarkpro.json"
|
||||
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
|
||||
|
||||
export const oc1Theme = oc1ThemeJson as DesktopTheme
|
||||
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
|
||||
export const draculaTheme = draculaThemeJson as DesktopTheme
|
||||
export const monokaiTheme = monokaiThemeJson as DesktopTheme
|
||||
export const solarizedTheme = solarizedThemeJson as DesktopTheme
|
||||
export const nordTheme = nordThemeJson as DesktopTheme
|
||||
export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
|
||||
export const ayuTheme = ayuThemeJson as DesktopTheme
|
||||
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
|
||||
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
||||
"oc-1": oc1Theme,
|
||||
tokyonight: tokyonightTheme,
|
||||
dracula: draculaTheme,
|
||||
monokai: monokaiTheme,
|
||||
solarized: solarizedTheme,
|
||||
nord: nordTheme,
|
||||
catppuccin: catppuccinTheme,
|
||||
ayu: ayuTheme,
|
||||
onedarkpro: oneDarkProTheme,
|
||||
shadesofpurple: shadesOfPurpleTheme,
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://opencode.ai/desktop-theme.json",
|
||||
"title": "OpenCode Desktop Theme",
|
||||
"description": "A theme definition for the OpenCode desktop application",
|
||||
"type": "object",
|
||||
"required": ["name", "id", "light", "dark"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "JSON Schema reference"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable theme name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique theme identifier (slug)",
|
||||
"pattern": "^[a-z0-9-]+$"
|
||||
},
|
||||
"light": {
|
||||
"$ref": "#/definitions/ThemeVariant",
|
||||
"description": "Light mode color variant"
|
||||
},
|
||||
"dark": {
|
||||
"$ref": "#/definitions/ThemeVariant",
|
||||
"description": "Dark mode color variant"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"HexColor": {
|
||||
"type": "string",
|
||||
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
|
||||
"description": "A hex color value like #fff, #ffff, #ffffff, or #ffffffff"
|
||||
},
|
||||
"ColorValue": {
|
||||
"type": "string",
|
||||
"pattern": "^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|var\(--[a-z0-9-]+\))$",
|
||||
"description": "Either a hex color value (#rgb/#rgba/#rrggbb/#rrggbbaa) or a CSS variable reference"
|
||||
},
|
||||
"ThemeSeedColors": {
|
||||
"type": "object",
|
||||
"description": "The minimum set of colors needed to generate a theme",
|
||||
"required": ["neutral", "primary", "success", "warning", "error", "info", "interactive", "diffAdd", "diffDelete"],
|
||||
"properties": {
|
||||
"neutral": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Base neutral color for generating the gray scale"
|
||||
},
|
||||
"primary": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Primary brand/accent color"
|
||||
},
|
||||
"success": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Success state color (typically green)"
|
||||
},
|
||||
"warning": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Warning state color (typically yellow/orange)"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Error/critical state color (typically red)"
|
||||
},
|
||||
"info": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Informational state color (typically purple/blue)"
|
||||
},
|
||||
"interactive": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Interactive element color (links, buttons)"
|
||||
},
|
||||
"diffAdd": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Color for diff additions"
|
||||
},
|
||||
"diffDelete": {
|
||||
"$ref": "#/definitions/HexColor",
|
||||
"description": "Color for diff deletions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ThemeVariant": {
|
||||
"type": "object",
|
||||
"description": "A theme variant (light or dark) with seed colors and optional overrides",
|
||||
"required": ["seeds"],
|
||||
"properties": {
|
||||
"seeds": {
|
||||
"$ref": "#/definitions/ThemeSeedColors",
|
||||
"description": "Seed colors used to generate the full palette"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "object",
|
||||
"description": "Optional direct overrides for any CSS variable (without -- prefix)",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/ColorValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type {
|
||||
DesktopTheme,
|
||||
ThemeSeedColors,
|
||||
ThemeVariant,
|
||||
HexColor,
|
||||
OklchColor,
|
||||
ResolvedTheme,
|
||||
ColorValue,
|
||||
CssVarRef,
|
||||
} from "./types"
|
||||
|
||||
export {
|
||||
hexToRgb,
|
||||
rgbToHex,
|
||||
hexToOklch,
|
||||
oklchToHex,
|
||||
rgbToOklch,
|
||||
oklchToRgb,
|
||||
generateScale,
|
||||
generateNeutralScale,
|
||||
generateAlphaScale,
|
||||
mixColors,
|
||||
lighten,
|
||||
darken,
|
||||
withAlpha,
|
||||
} from "./color"
|
||||
|
||||
export { resolveThemeVariant, resolveTheme, themeToCss } from "./resolve"
|
||||
export { applyTheme, loadThemeFromUrl, getActiveTheme, removeTheme, setColorScheme } from "./loader"
|
||||
export { ThemeProvider, useTheme, type ColorScheme } from "./context"
|
||||
|
||||
export {
|
||||
DEFAULT_THEMES,
|
||||
oc1Theme,
|
||||
tokyonightTheme,
|
||||
draculaTheme,
|
||||
monokaiTheme,
|
||||
solarizedTheme,
|
||||
nordTheme,
|
||||
catppuccinTheme,
|
||||
ayuTheme,
|
||||
oneDarkProTheme,
|
||||
shadesOfPurpleTheme,
|
||||
} from "./default-themes"
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { DesktopTheme, ResolvedTheme } from "./types"
|
||||
import { resolveThemeVariant, themeToCss } from "./resolve"
|
||||
|
||||
let activeTheme: DesktopTheme | null = null
|
||||
const THEME_STYLE_ID = "opencode-theme"
|
||||
|
||||
function ensureLoaderStyleElement(): HTMLStyleElement {
|
||||
const existing = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
const element = document.createElement("style")
|
||||
element.id = THEME_STYLE_ID
|
||||
document.head.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
export function applyTheme(theme: DesktopTheme, themeId?: string): void {
|
||||
activeTheme = theme
|
||||
const lightTokens = resolveThemeVariant(theme.light, false)
|
||||
const darkTokens = resolveThemeVariant(theme.dark, true)
|
||||
const targetThemeId = themeId ?? theme.id
|
||||
const css = buildThemeCss(lightTokens, darkTokens, targetThemeId)
|
||||
const themeStyleElement = ensureLoaderStyleElement()
|
||||
themeStyleElement.textContent = css
|
||||
document.documentElement.setAttribute("data-theme", targetThemeId)
|
||||
}
|
||||
|
||||
function buildThemeCss(light: ResolvedTheme, dark: ResolvedTheme, themeId: string): string {
|
||||
const isDefaultTheme = themeId === "oc-1"
|
||||
const lightCss = themeToCss(light)
|
||||
const darkCss = themeToCss(dark)
|
||||
|
||||
if (isDefaultTheme) {
|
||||
return `
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--text-mix-blend-mode: multiply;
|
||||
|
||||
${lightCss}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
--text-mix-blend-mode: plus-lighter;
|
||||
|
||||
${darkCss}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
html[data-theme="${themeId}"] {
|
||||
color-scheme: light;
|
||||
--text-mix-blend-mode: multiply;
|
||||
|
||||
${lightCss}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
--text-mix-blend-mode: plus-lighter;
|
||||
|
||||
${darkCss}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export async function loadThemeFromUrl(url: string): Promise<DesktopTheme> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load theme from ${url}: ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export function getActiveTheme(): DesktopTheme | null {
|
||||
const activeId = document.documentElement.getAttribute("data-theme")
|
||||
if (!activeId) {
|
||||
return null
|
||||
}
|
||||
if (activeTheme?.id === activeId) {
|
||||
return activeTheme
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function removeTheme(): void {
|
||||
activeTheme = null
|
||||
const existingElement = document.getElementById(THEME_STYLE_ID)
|
||||
if (existingElement) {
|
||||
existingElement.remove()
|
||||
}
|
||||
document.documentElement.removeAttribute("data-theme")
|
||||
}
|
||||
|
||||
export function setColorScheme(scheme: "light" | "dark" | "auto"): void {
|
||||
if (scheme === "auto") {
|
||||
document.documentElement.style.removeProperty("color-scheme")
|
||||
} else {
|
||||
document.documentElement.style.setProperty("color-scheme", scheme)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
import type { ColorValue, DesktopTheme, HexColor, ResolvedTheme, ThemeVariant } from "./types"
|
||||
import { generateNeutralScale, generateScale, hexToOklch, oklchToHex, withAlpha } from "./color"
|
||||
|
||||
export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): ResolvedTheme {
|
||||
const { seeds, overrides = {} } = variant
|
||||
|
||||
const neutral = generateNeutralScale(seeds.neutral, isDark)
|
||||
const primary = generateScale(seeds.primary, isDark)
|
||||
const success = generateScale(seeds.success, isDark)
|
||||
const warning = generateScale(seeds.warning, isDark)
|
||||
const error = generateScale(seeds.error, isDark)
|
||||
const info = generateScale(seeds.info, isDark)
|
||||
const interactive = generateScale(seeds.interactive, isDark)
|
||||
const diffAdd = generateScale(seeds.diffAdd, isDark)
|
||||
const diffDelete = generateScale(seeds.diffDelete, isDark)
|
||||
|
||||
const neutralAlpha = generateNeutralAlphaScale(neutral, isDark)
|
||||
|
||||
const tokens: ResolvedTheme = {}
|
||||
|
||||
tokens["background-base"] = neutral[0]
|
||||
tokens["background-weak"] = neutral[2]
|
||||
tokens["background-strong"] = neutral[0]
|
||||
tokens["background-stronger"] = isDark ? neutral[1] : "#fcfcfc"
|
||||
|
||||
tokens["surface-base"] = neutralAlpha[1]
|
||||
tokens["base"] = neutralAlpha[1]
|
||||
tokens["surface-base-hover"] = neutralAlpha[2]
|
||||
tokens["surface-base-active"] = neutralAlpha[2]
|
||||
tokens["surface-base-interactive-active"] = withAlpha(interactive[2], 0.3) as ColorValue
|
||||
tokens["base2"] = neutralAlpha[1]
|
||||
tokens["base3"] = neutralAlpha[1]
|
||||
tokens["surface-inset-base"] = neutralAlpha[1]
|
||||
tokens["surface-inset-base-hover"] = neutralAlpha[2]
|
||||
tokens["surface-inset-strong"] = isDark
|
||||
? (withAlpha(neutral[0], 0.5) as ColorValue)
|
||||
: (withAlpha(neutral[3], 0.09) as ColorValue)
|
||||
tokens["surface-inset-strong-hover"] = tokens["surface-inset-strong"]
|
||||
tokens["surface-raised-base"] = neutralAlpha[0]
|
||||
tokens["surface-float-base"] = isDark ? neutral[0] : neutral[11]
|
||||
tokens["surface-float-base-hover"] = isDark ? neutral[1] : neutral[10]
|
||||
tokens["surface-raised-base-hover"] = neutralAlpha[1]
|
||||
tokens["surface-raised-base-active"] = neutralAlpha[2]
|
||||
tokens["surface-raised-strong"] = isDark ? neutralAlpha[3] : neutral[0]
|
||||
tokens["surface-raised-strong-hover"] = isDark ? neutralAlpha[5] : "#ffffff"
|
||||
tokens["surface-raised-stronger"] = isDark ? neutralAlpha[5] : "#ffffff"
|
||||
tokens["surface-raised-stronger-hover"] = isDark ? neutralAlpha[6] : "#ffffff"
|
||||
tokens["surface-weak"] = neutralAlpha[2]
|
||||
tokens["surface-weaker"] = neutralAlpha[3]
|
||||
tokens["surface-strong"] = isDark ? neutralAlpha[6] : "#ffffff"
|
||||
tokens["surface-raised-stronger-non-alpha"] = isDark ? neutral[2] : "#ffffff"
|
||||
|
||||
tokens["surface-brand-base"] = primary[8]
|
||||
tokens["surface-brand-hover"] = primary[9]
|
||||
|
||||
tokens["surface-interactive-base"] = interactive[2]
|
||||
tokens["surface-interactive-hover"] = interactive[3]
|
||||
tokens["surface-interactive-weak"] = interactive[1]
|
||||
tokens["surface-interactive-weak-hover"] = interactive[2]
|
||||
|
||||
tokens["surface-success-base"] = success[2]
|
||||
tokens["surface-success-weak"] = success[1]
|
||||
tokens["surface-success-strong"] = success[8]
|
||||
tokens["surface-warning-base"] = warning[2]
|
||||
tokens["surface-warning-weak"] = warning[1]
|
||||
tokens["surface-warning-strong"] = warning[8]
|
||||
tokens["surface-critical-base"] = error[2]
|
||||
tokens["surface-critical-weak"] = error[1]
|
||||
tokens["surface-critical-strong"] = error[8]
|
||||
tokens["surface-info-base"] = info[2]
|
||||
tokens["surface-info-weak"] = info[1]
|
||||
tokens["surface-info-strong"] = info[8]
|
||||
|
||||
tokens["surface-diff-unchanged-base"] = isDark ? neutral[0] : "#ffffff00"
|
||||
tokens["surface-diff-skip-base"] = isDark ? neutralAlpha[0] : neutral[1]
|
||||
tokens["surface-diff-hidden-base"] = interactive[isDark ? 1 : 2]
|
||||
tokens["surface-diff-hidden-weak"] = interactive[isDark ? 0 : 1]
|
||||
tokens["surface-diff-hidden-weaker"] = interactive[isDark ? 2 : 0]
|
||||
tokens["surface-diff-hidden-strong"] = interactive[4]
|
||||
tokens["surface-diff-hidden-stronger"] = interactive[isDark ? 10 : 8]
|
||||
tokens["surface-diff-add-base"] = diffAdd[2]
|
||||
tokens["surface-diff-add-weak"] = diffAdd[isDark ? 3 : 1]
|
||||
tokens["surface-diff-add-weaker"] = diffAdd[isDark ? 2 : 0]
|
||||
tokens["surface-diff-add-strong"] = diffAdd[4]
|
||||
tokens["surface-diff-add-stronger"] = diffAdd[isDark ? 10 : 8]
|
||||
tokens["surface-diff-delete-base"] = diffDelete[2]
|
||||
tokens["surface-diff-delete-weak"] = diffDelete[isDark ? 3 : 1]
|
||||
tokens["surface-diff-delete-weaker"] = diffDelete[isDark ? 2 : 0]
|
||||
tokens["surface-diff-delete-strong"] = diffDelete[isDark ? 4 : 5]
|
||||
tokens["surface-diff-delete-stronger"] = diffDelete[isDark ? 10 : 8]
|
||||
|
||||
tokens["input-base"] = isDark ? neutral[1] : neutral[0]
|
||||
tokens["input-hover"] = neutral[1]
|
||||
tokens["input-active"] = interactive[0]
|
||||
tokens["input-selected"] = interactive[3]
|
||||
tokens["input-focus"] = interactive[0]
|
||||
tokens["input-disabled"] = neutral[3]
|
||||
|
||||
tokens["text-base"] = neutral[10]
|
||||
tokens["text-weak"] = neutral[8]
|
||||
tokens["text-weaker"] = neutral[7]
|
||||
tokens["text-strong"] = neutral[11]
|
||||
tokens["text-invert-base"] = isDark ? neutral[10] : neutralAlpha[10]
|
||||
tokens["text-invert-weak"] = isDark ? neutral[8] : neutralAlpha[8]
|
||||
tokens["text-invert-weaker"] = isDark ? neutral[7] : neutralAlpha[7]
|
||||
tokens["text-invert-strong"] = isDark ? neutral[11] : neutralAlpha[11]
|
||||
tokens["text-interactive-base"] = interactive[isDark ? 10 : 8]
|
||||
tokens["text-on-brand-base"] = neutralAlpha[10]
|
||||
tokens["text-on-interactive-base"] = isDark ? neutral[11] : neutral[0]
|
||||
tokens["text-on-interactive-weak"] = neutralAlpha[10]
|
||||
tokens["text-on-success-base"] = success[isDark ? 8 : 9]
|
||||
tokens["text-on-critical-base"] = error[isDark ? 8 : 9]
|
||||
tokens["text-on-critical-weak"] = error[7]
|
||||
tokens["text-on-critical-strong"] = error[11]
|
||||
tokens["text-on-warning-base"] = neutralAlpha[10]
|
||||
tokens["text-on-info-base"] = neutralAlpha[10]
|
||||
tokens["text-diff-add-base"] = diffAdd[10]
|
||||
tokens["text-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
|
||||
tokens["text-diff-delete-strong"] = diffDelete[11]
|
||||
tokens["text-diff-add-strong"] = diffAdd[isDark ? 7 : 11]
|
||||
tokens["text-on-info-weak"] = neutralAlpha[8]
|
||||
tokens["text-on-info-strong"] = neutralAlpha[11]
|
||||
tokens["text-on-warning-weak"] = neutralAlpha[8]
|
||||
tokens["text-on-warning-strong"] = neutralAlpha[11]
|
||||
tokens["text-on-success-weak"] = success[isDark ? 7 : 5]
|
||||
tokens["text-on-success-strong"] = success[11]
|
||||
tokens["text-on-brand-weak"] = neutralAlpha[8]
|
||||
tokens["text-on-brand-weaker"] = neutralAlpha[7]
|
||||
tokens["text-on-brand-strong"] = neutralAlpha[11]
|
||||
|
||||
tokens["button-secondary-base"] = isDark ? neutral[2] : neutral[0]
|
||||
tokens["button-secondary-hover"] = isDark ? neutral[3] : neutral[1]
|
||||
tokens["button-ghost-hover"] = neutralAlpha[1]
|
||||
tokens["button-ghost-hover2"] = neutralAlpha[2]
|
||||
|
||||
tokens["border-base"] = neutralAlpha[6]
|
||||
tokens["border-hover"] = neutralAlpha[7]
|
||||
tokens["border-active"] = neutralAlpha[8]
|
||||
tokens["border-selected"] = withAlpha(interactive[8], isDark ? 0.9 : 0.99) as ColorValue
|
||||
tokens["border-disabled"] = neutralAlpha[7]
|
||||
tokens["border-focus"] = neutralAlpha[8]
|
||||
tokens["border-weak-base"] = neutralAlpha[isDark ? 5 : 4]
|
||||
tokens["border-strong-base"] = neutralAlpha[isDark ? 7 : 6]
|
||||
tokens["border-strong-hover"] = neutralAlpha[7]
|
||||
tokens["border-strong-active"] = neutralAlpha[isDark ? 7 : 6]
|
||||
tokens["border-strong-selected"] = withAlpha(interactive[5], 0.6) as ColorValue
|
||||
tokens["border-strong-disabled"] = neutralAlpha[5]
|
||||
tokens["border-strong-focus"] = neutralAlpha[isDark ? 7 : 6]
|
||||
tokens["border-weak-hover"] = neutralAlpha[isDark ? 6 : 5]
|
||||
tokens["border-weak-active"] = neutralAlpha[isDark ? 7 : 6]
|
||||
tokens["border-weak-selected"] = withAlpha(interactive[4], isDark ? 0.6 : 0.5) as ColorValue
|
||||
tokens["border-weak-disabled"] = neutralAlpha[5]
|
||||
tokens["border-weak-focus"] = neutralAlpha[isDark ? 7 : 6]
|
||||
tokens["border-weaker-base"] = neutralAlpha[2]
|
||||
tokens["border-weaker-hover"] = neutralAlpha[3]
|
||||
tokens["border-weaker-active"] = neutralAlpha[5]
|
||||
tokens["border-weaker-selected"] = withAlpha(interactive[3], isDark ? 0.3 : 0.4) as ColorValue
|
||||
tokens["border-weaker-disabled"] = neutralAlpha[1]
|
||||
tokens["border-weaker-focus"] = neutralAlpha[5]
|
||||
|
||||
tokens["border-interactive-base"] = interactive[6]
|
||||
tokens["border-interactive-hover"] = interactive[7]
|
||||
tokens["border-interactive-active"] = interactive[8]
|
||||
tokens["border-interactive-selected"] = interactive[8]
|
||||
tokens["border-interactive-disabled"] = neutral[7]
|
||||
tokens["border-interactive-focus"] = interactive[8]
|
||||
|
||||
tokens["border-success-base"] = success[5]
|
||||
tokens["border-success-hover"] = success[6]
|
||||
tokens["border-success-selected"] = success[8]
|
||||
tokens["border-warning-base"] = warning[5]
|
||||
tokens["border-warning-hover"] = warning[6]
|
||||
tokens["border-warning-selected"] = warning[8]
|
||||
tokens["border-critical-base"] = error[isDark ? 4 : 5]
|
||||
tokens["border-critical-hover"] = error[6]
|
||||
tokens["border-critical-selected"] = error[8]
|
||||
tokens["border-info-base"] = info[5]
|
||||
tokens["border-info-hover"] = info[6]
|
||||
tokens["border-info-selected"] = info[8]
|
||||
tokens["border-color"] = "#ffffff"
|
||||
|
||||
tokens["icon-base"] = neutral[8]
|
||||
tokens["icon-hover"] = neutral[isDark ? 9 : 10]
|
||||
tokens["icon-active"] = neutral[isDark ? 10 : 11]
|
||||
tokens["icon-selected"] = neutral[11]
|
||||
tokens["icon-disabled"] = neutral[isDark ? 6 : 7]
|
||||
tokens["icon-focus"] = neutral[11]
|
||||
tokens["icon-invert-base"] = isDark ? neutral[0] : "#ffffff"
|
||||
tokens["icon-weak-base"] = neutral[isDark ? 5 : 6]
|
||||
tokens["icon-weak-hover"] = neutral[6]
|
||||
tokens["icon-weak-active"] = neutral[7]
|
||||
tokens["icon-weak-selected"] = neutral[8]
|
||||
tokens["icon-weak-disabled"] = neutral[isDark ? 3 : 5]
|
||||
tokens["icon-weak-focus"] = neutral[8]
|
||||
tokens["icon-strong-base"] = neutral[11]
|
||||
tokens["icon-strong-hover"] = isDark ? "#f6f3f3" : "#151313"
|
||||
tokens["icon-strong-active"] = isDark ? "#fcfcfc" : "#020202"
|
||||
tokens["icon-strong-selected"] = isDark ? "#fdfcfc" : "#020202"
|
||||
tokens["icon-strong-disabled"] = neutral[7]
|
||||
tokens["icon-strong-focus"] = isDark ? "#fdfcfc" : "#020202"
|
||||
tokens["icon-brand-base"] = isDark ? "#ffffff" : neutral[11]
|
||||
tokens["icon-interactive-base"] = interactive[8]
|
||||
tokens["icon-success-base"] = success[isDark ? 6 : 6]
|
||||
tokens["icon-success-hover"] = success[7]
|
||||
tokens["icon-success-active"] = success[10]
|
||||
tokens["icon-warning-base"] = warning[6]
|
||||
tokens["icon-warning-hover"] = warning[7]
|
||||
tokens["icon-warning-active"] = warning[10]
|
||||
tokens["icon-critical-base"] = error[isDark ? 8 : 9]
|
||||
tokens["icon-critical-hover"] = error[10]
|
||||
tokens["icon-critical-active"] = error[11]
|
||||
tokens["icon-info-base"] = info[isDark ? 6 : 6]
|
||||
tokens["icon-info-hover"] = info[7]
|
||||
tokens["icon-info-active"] = info[10]
|
||||
tokens["icon-on-brand-base"] = neutralAlpha[10]
|
||||
tokens["icon-on-brand-hover"] = neutralAlpha[11]
|
||||
tokens["icon-on-brand-selected"] = neutralAlpha[11]
|
||||
tokens["icon-on-interactive-base"] = isDark ? neutral[11] : neutral[0]
|
||||
|
||||
tokens["icon-agent-plan-base"] = info[8]
|
||||
tokens["icon-agent-docs-base"] = warning[8]
|
||||
tokens["icon-agent-ask-base"] = interactive[8]
|
||||
tokens["icon-agent-build-base"] = interactive[isDark ? 10 : 8]
|
||||
|
||||
tokens["icon-on-success-base"] = withAlpha(success[8], 0.9) as ColorValue
|
||||
tokens["icon-on-success-hover"] = withAlpha(success[9], 0.9) as ColorValue
|
||||
tokens["icon-on-success-selected"] = withAlpha(success[10], 0.9) as ColorValue
|
||||
tokens["icon-on-warning-base"] = withAlpha(warning[8], 0.9) as ColorValue
|
||||
tokens["icon-on-warning-hover"] = withAlpha(warning[9], 0.9) as ColorValue
|
||||
tokens["icon-on-warning-selected"] = withAlpha(warning[10], 0.9) as ColorValue
|
||||
tokens["icon-on-critical-base"] = withAlpha(error[8], 0.9) as ColorValue
|
||||
tokens["icon-on-critical-hover"] = withAlpha(error[9], 0.9) as ColorValue
|
||||
tokens["icon-on-critical-selected"] = withAlpha(error[10], 0.9) as ColorValue
|
||||
tokens["icon-on-info-base"] = info[8]
|
||||
tokens["icon-on-info-hover"] = withAlpha(info[9], 0.9) as ColorValue
|
||||
tokens["icon-on-info-selected"] = withAlpha(info[10], 0.9) as ColorValue
|
||||
|
||||
tokens["icon-diff-add-base"] = diffAdd[10]
|
||||
tokens["icon-diff-add-hover"] = diffAdd[isDark ? 9 : 11]
|
||||
tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
|
||||
tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
|
||||
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
|
||||
|
||||
tokens["syntax-comment"] = "var(--text-weak)"
|
||||
tokens["syntax-regexp"] = "var(--text-base)"
|
||||
tokens["syntax-string"] = isDark ? "#00ceb9" : "#006656"
|
||||
tokens["syntax-keyword"] = "var(--text-weak)"
|
||||
tokens["syntax-primitive"] = isDark ? "#ffba92" : "#fb4804"
|
||||
tokens["syntax-operator"] = isDark ? "var(--text-weak)" : "var(--text-base)"
|
||||
tokens["syntax-variable"] = "var(--text-strong)"
|
||||
tokens["syntax-property"] = isDark ? "#ff9ae2" : "#ed6dc8"
|
||||
tokens["syntax-type"] = isDark ? "#ecf58c" : "#596600"
|
||||
tokens["syntax-constant"] = isDark ? "#93e9f6" : "#007b80"
|
||||
tokens["syntax-punctuation"] = isDark ? "var(--text-weak)" : "var(--text-base)"
|
||||
tokens["syntax-object"] = "var(--text-strong)"
|
||||
tokens["syntax-success"] = success[9]
|
||||
tokens["syntax-warning"] = warning[9]
|
||||
tokens["syntax-critical"] = error[isDark ? 9 : 9]
|
||||
tokens["syntax-info"] = isDark ? "#93e9f6" : "#0092a8"
|
||||
tokens["syntax-diff-add"] = diffAdd[10]
|
||||
tokens["syntax-diff-delete"] = diffDelete[10]
|
||||
tokens["syntax-diff-unknown"] = "#ff0000"
|
||||
|
||||
tokens["markdown-heading"] = isDark ? "#9d7cd8" : "#d68c27"
|
||||
tokens["markdown-text"] = isDark ? "#eeeeee" : "#1a1a1a"
|
||||
tokens["markdown-link"] = isDark ? "#fab283" : "#3b7dd8"
|
||||
tokens["markdown-link-text"] = isDark ? "#56b6c2" : "#318795"
|
||||
tokens["markdown-code"] = isDark ? "#7fd88f" : "#3d9a57"
|
||||
tokens["markdown-block-quote"] = isDark ? "#e5c07b" : "#b0851f"
|
||||
tokens["markdown-emph"] = isDark ? "#e5c07b" : "#b0851f"
|
||||
tokens["markdown-strong"] = isDark ? "#f5a742" : "#d68c27"
|
||||
tokens["markdown-horizontal-rule"] = isDark ? "#808080" : "#8a8a8a"
|
||||
tokens["markdown-list-item"] = isDark ? "#fab283" : "#3b7dd8"
|
||||
tokens["markdown-list-enumeration"] = isDark ? "#56b6c2" : "#318795"
|
||||
tokens["markdown-image"] = isDark ? "#fab283" : "#3b7dd8"
|
||||
tokens["markdown-image-text"] = isDark ? "#56b6c2" : "#318795"
|
||||
tokens["markdown-code-block"] = isDark ? "#eeeeee" : "#1a1a1a"
|
||||
|
||||
tokens["avatar-background-pink"] = isDark ? "#501b3f" : "#feeef8"
|
||||
tokens["avatar-background-mint"] = isDark ? "#033a34" : "#e1fbf4"
|
||||
tokens["avatar-background-orange"] = isDark ? "#5f2a06" : "#fff1e7"
|
||||
tokens["avatar-background-purple"] = isDark ? "#432155" : "#f9f1fe"
|
||||
tokens["avatar-background-cyan"] = isDark ? "#0f3058" : "#e7f9fb"
|
||||
tokens["avatar-background-lime"] = isDark ? "#2b3711" : "#eefadc"
|
||||
tokens["avatar-text-pink"] = isDark ? "#e34ba9" : "#cd1d8d"
|
||||
tokens["avatar-text-mint"] = isDark ? "#95f3d9" : "#147d6f"
|
||||
tokens["avatar-text-orange"] = isDark ? "#ff802b" : "#ed5f00"
|
||||
tokens["avatar-text-purple"] = isDark ? "#9d5bd2" : "#8445bc"
|
||||
tokens["avatar-text-cyan"] = isDark ? "#369eff" : "#0894b3"
|
||||
tokens["avatar-text-lime"] = isDark ? "#c4f042" : "#5d770d"
|
||||
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
tokens[key] = value
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
function generateNeutralAlphaScale(neutralScale: HexColor[], isDark: boolean): HexColor[] {
|
||||
const alphas = isDark
|
||||
? [0.02, 0.04, 0.08, 0.12, 0.16, 0.2, 0.26, 0.36, 0.44, 0.52, 0.72, 0.94]
|
||||
: [0.01, 0.03, 0.06, 0.09, 0.12, 0.15, 0.2, 0.27, 0.46, 0.61, 0.5, 0.87]
|
||||
|
||||
return neutralScale.map((hex, i) => {
|
||||
const baseOklch = hexToOklch(hex)
|
||||
const targetL = isDark ? 0.1 + alphas[i] * 0.8 : 1 - alphas[i] * 0.8
|
||||
return oklchToHex({
|
||||
...baseOklch,
|
||||
l: baseOklch.l * alphas[i] + targetL * (1 - alphas[i]),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveTheme(theme: DesktopTheme): { light: ResolvedTheme; dark: ResolvedTheme } {
|
||||
return {
|
||||
light: resolveThemeVariant(theme.light, false),
|
||||
dark: resolveThemeVariant(theme.dark, true),
|
||||
}
|
||||
}
|
||||
|
||||
export function themeToCss(tokens: ResolvedTheme): string {
|
||||
return Object.entries(tokens)
|
||||
.map(([key, value]) => `--${key}: ${value};`)
|
||||
.join("\n ")
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Ayu",
|
||||
"id": "ayu",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#fdfaf4",
|
||||
"primary": "#55b4d4",
|
||||
"success": "#6ac782",
|
||||
"warning": "#f2ae49",
|
||||
"error": "#f05f65",
|
||||
"info": "#36a3d9",
|
||||
"interactive": "#55b4d4",
|
||||
"diffAdd": "#b8df8a",
|
||||
"diffDelete": "#f05f65"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#fdfaf4",
|
||||
"background-weak": "#f6f0e7",
|
||||
"background-strong": "#f1ebe2",
|
||||
"background-stronger": "#ece4da",
|
||||
"border-weak-base": "#e6ddcf",
|
||||
"border-weak-hover": "#dcd3c5",
|
||||
"border-weak-active": "#d1c9ba",
|
||||
"border-weak-selected": "#c6bfaf",
|
||||
"border-weak-disabled": "#f7f0e6",
|
||||
"border-weak-focus": "#cbc4b6",
|
||||
"border-base": "#bfb3a3",
|
||||
"border-hover": "#b4a898",
|
||||
"border-active": "#a99e8e",
|
||||
"border-selected": "#9e9383",
|
||||
"border-disabled": "#efe5d8",
|
||||
"border-focus": "#b09f8f",
|
||||
"border-strong-base": "#8f806f",
|
||||
"border-strong-hover": "#837465",
|
||||
"border-strong-active": "#77685a",
|
||||
"border-strong-selected": "#6b5d51",
|
||||
"border-strong-disabled": "#d8cabc",
|
||||
"border-strong-focus": "#7c6d5e",
|
||||
"surface-diff-add-base": "#eef5e4",
|
||||
"surface-diff-delete-base": "#fde5e5",
|
||||
"surface-diff-hidden-base": "#e3edf3",
|
||||
"text-base": "#5c6773",
|
||||
"text-weak": "#8a939f",
|
||||
"text-strong": "#2a3038",
|
||||
"syntax-string": "#86b300",
|
||||
"syntax-primitive": "#f28779",
|
||||
"syntax-property": "#55b4d4",
|
||||
"syntax-type": "#f29e32",
|
||||
"syntax-constant": "#36a3d9",
|
||||
"syntax-info": "#36a3d9",
|
||||
"markdown-heading": "#55b4d4",
|
||||
"markdown-text": "#5c6773",
|
||||
"markdown-link": "#55b4d4",
|
||||
"markdown-link-text": "#36a3d9",
|
||||
"markdown-code": "#86b300",
|
||||
"markdown-block-quote": "#f29e32",
|
||||
"markdown-emph": "#f29e32",
|
||||
"markdown-strong": "#f28779",
|
||||
"markdown-horizontal-rule": "#d7cec0",
|
||||
"markdown-list-item": "#55b4d4",
|
||||
"markdown-list-enumeration": "#36a3d9",
|
||||
"markdown-image": "#55b4d4",
|
||||
"markdown-image-text": "#36a3d9",
|
||||
"markdown-code-block": "#55b4d4"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#0f1419",
|
||||
"primary": "#39bae6",
|
||||
"success": "#7fd962",
|
||||
"warning": "#ebb062",
|
||||
"error": "#ff8f77",
|
||||
"info": "#73d0ff",
|
||||
"interactive": "#39bae6",
|
||||
"diffAdd": "#5cc885",
|
||||
"diffDelete": "#ff8f77"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#0f1419",
|
||||
"background-weak": "#121920",
|
||||
"background-strong": "#0d1116",
|
||||
"background-stronger": "#0a0e13",
|
||||
"border-weak-base": "#262c34",
|
||||
"border-weak-hover": "#2b323d",
|
||||
"border-weak-active": "#303746",
|
||||
"border-weak-selected": "#363d50",
|
||||
"border-weak-disabled": "#080b0f",
|
||||
"border-weak-focus": "#323a48",
|
||||
"border-base": "#3d4555",
|
||||
"border-hover": "#454d61",
|
||||
"border-active": "#4c556d",
|
||||
"border-selected": "#545d79",
|
||||
"border-disabled": "#0e1218",
|
||||
"border-focus": "#495368",
|
||||
"border-strong-base": "#626c81",
|
||||
"border-strong-hover": "#6c7690",
|
||||
"border-strong-active": "#76819f",
|
||||
"border-strong-selected": "#808bae",
|
||||
"border-strong-disabled": "#151b23",
|
||||
"border-strong-focus": "#6f7a96",
|
||||
"surface-diff-add-base": "#102922",
|
||||
"surface-diff-delete-base": "#2b1718",
|
||||
"surface-diff-hidden-base": "#182028",
|
||||
"text-base": "#ced0d6",
|
||||
"text-weak": "#8f9aa5",
|
||||
"text-strong": "#f6f7f9",
|
||||
"syntax-string": "#b8cc52",
|
||||
"syntax-primitive": "#f59074",
|
||||
"syntax-property": "#39bae6",
|
||||
"syntax-type": "#ebb062",
|
||||
"syntax-constant": "#73d0ff",
|
||||
"syntax-info": "#73d0ff",
|
||||
"markdown-heading": "#39bae6",
|
||||
"markdown-text": "#ced0d6",
|
||||
"markdown-link": "#39bae6",
|
||||
"markdown-link-text": "#73d0ff",
|
||||
"markdown-code": "#b8cc52",
|
||||
"markdown-block-quote": "#ebb062",
|
||||
"markdown-emph": "#ebb062",
|
||||
"markdown-strong": "#f59074",
|
||||
"markdown-horizontal-rule": "#1f2630",
|
||||
"markdown-list-item": "#39bae6",
|
||||
"markdown-list-enumeration": "#73d0ff",
|
||||
"markdown-image": "#39bae6",
|
||||
"markdown-image-text": "#73d0ff",
|
||||
"markdown-code-block": "#ced0d6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Catppuccin",
|
||||
"id": "catppuccin",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#f5e0dc",
|
||||
"primary": "#7287fd",
|
||||
"success": "#40a02b",
|
||||
"warning": "#df8e1d",
|
||||
"error": "#d20f39",
|
||||
"info": "#04a5e5",
|
||||
"interactive": "#7287fd",
|
||||
"diffAdd": "#a6d189",
|
||||
"diffDelete": "#e78284"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#f5e0dc",
|
||||
"background-weak": "#f2d8d4",
|
||||
"background-strong": "#f9e8e4",
|
||||
"background-stronger": "#fdeeee",
|
||||
"border-weak-base": "#e0cfd3",
|
||||
"border-weak-hover": "#d6c4c8",
|
||||
"border-weak-active": "#cdb9be",
|
||||
"border-weak-selected": "#c2aeb4",
|
||||
"border-weak-disabled": "#fbeff2",
|
||||
"border-weak-focus": "#c7b4ba",
|
||||
"border-base": "#bca6b2",
|
||||
"border-hover": "#b19ca8",
|
||||
"border-active": "#a6929e",
|
||||
"border-selected": "#9a8894",
|
||||
"border-disabled": "#f3e4e7",
|
||||
"border-focus": "#ab97a1",
|
||||
"border-strong-base": "#83677f",
|
||||
"border-strong-hover": "#775b73",
|
||||
"border-strong-active": "#6b5068",
|
||||
"border-strong-selected": "#5f465d",
|
||||
"border-strong-disabled": "#d9c5cf",
|
||||
"border-strong-focus": "#714f66",
|
||||
"surface-diff-add-base": "#edf5e6",
|
||||
"surface-diff-delete-base": "#fde1e3",
|
||||
"surface-diff-hidden-base": "#e4e2f6",
|
||||
"text-base": "#4c4f69",
|
||||
"text-weak": "#6c6f85",
|
||||
"text-strong": "#1f1f2a",
|
||||
"syntax-string": "#40a02b",
|
||||
"syntax-primitive": "#d20f39",
|
||||
"syntax-property": "#7287fd",
|
||||
"syntax-type": "#df8e1d",
|
||||
"syntax-constant": "#04a5e5",
|
||||
"syntax-info": "#04a5e5",
|
||||
"markdown-heading": "#7287fd",
|
||||
"markdown-text": "#4c4f69",
|
||||
"markdown-link": "#7287fd",
|
||||
"markdown-link-text": "#04a5e5",
|
||||
"markdown-code": "#40a02b",
|
||||
"markdown-block-quote": "#df8e1d",
|
||||
"markdown-emph": "#df8e1d",
|
||||
"markdown-strong": "#d20f39",
|
||||
"markdown-horizontal-rule": "#d4c5cf",
|
||||
"markdown-list-item": "#7287fd",
|
||||
"markdown-list-enumeration": "#04a5e5",
|
||||
"markdown-image": "#7287fd",
|
||||
"markdown-image-text": "#04a5e5",
|
||||
"markdown-code-block": "#7287fd"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#1e1e2e",
|
||||
"primary": "#b4befe",
|
||||
"success": "#a6d189",
|
||||
"warning": "#f4b8e4",
|
||||
"error": "#f38ba8",
|
||||
"info": "#89dceb",
|
||||
"interactive": "#b4befe",
|
||||
"diffAdd": "#94e2d5",
|
||||
"diffDelete": "#f38ba8"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#1e1e2e",
|
||||
"background-weak": "#211f31",
|
||||
"background-strong": "#1c1c29",
|
||||
"background-stronger": "#191926",
|
||||
"border-weak-base": "#35324a",
|
||||
"border-weak-hover": "#393655",
|
||||
"border-weak-active": "#403c61",
|
||||
"border-weak-selected": "#47436d",
|
||||
"border-weak-disabled": "#141426",
|
||||
"border-weak-focus": "#3d3a63",
|
||||
"border-base": "#4a4763",
|
||||
"border-hover": "#524f70",
|
||||
"border-active": "#5a577d",
|
||||
"border-selected": "#625f8a",
|
||||
"border-disabled": "#1b1a2c",
|
||||
"border-focus": "#575379",
|
||||
"border-strong-base": "#6e6a8c",
|
||||
"border-strong-hover": "#787497",
|
||||
"border-strong-active": "#8380a2",
|
||||
"border-strong-selected": "#8d8bad",
|
||||
"border-strong-disabled": "#232237",
|
||||
"border-strong-focus": "#7b779b",
|
||||
"surface-diff-add-base": "#1d2c30",
|
||||
"surface-diff-delete-base": "#2c1f2a",
|
||||
"surface-diff-hidden-base": "#232538",
|
||||
"text-base": "#cdd6f4",
|
||||
"text-weak": "#a6adc8",
|
||||
"text-strong": "#f4f2ff",
|
||||
"syntax-string": "#a6e3a1",
|
||||
"syntax-primitive": "#f38ba8",
|
||||
"syntax-property": "#b4befe",
|
||||
"syntax-type": "#f9e2af",
|
||||
"syntax-constant": "#89dceb",
|
||||
"syntax-info": "#89dceb",
|
||||
"markdown-heading": "#b4befe",
|
||||
"markdown-text": "#cdd6f4",
|
||||
"markdown-link": "#b4befe",
|
||||
"markdown-link-text": "#89dceb",
|
||||
"markdown-code": "#a6e3a1",
|
||||
"markdown-block-quote": "#f9e2af",
|
||||
"markdown-emph": "#f9e2af",
|
||||
"markdown-strong": "#f38ba8",
|
||||
"markdown-horizontal-rule": "#2e2d45",
|
||||
"markdown-list-item": "#b4befe",
|
||||
"markdown-list-enumeration": "#89dceb",
|
||||
"markdown-image": "#b4befe",
|
||||
"markdown-image-text": "#89dceb",
|
||||
"markdown-code-block": "#cdd6f4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Dracula",
|
||||
"id": "dracula",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#f8f8f2",
|
||||
"primary": "#7c6bf5",
|
||||
"success": "#2fbf71",
|
||||
"warning": "#f7a14d",
|
||||
"error": "#d9536f",
|
||||
"info": "#1d7fc5",
|
||||
"interactive": "#7c6bf5",
|
||||
"diffAdd": "#9fe3b3",
|
||||
"diffDelete": "#f8a1b8"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#f8f8f2",
|
||||
"background-weak": "#f1f2ed",
|
||||
"background-strong": "#f6f6f1",
|
||||
"background-stronger": "#f2f2ec",
|
||||
"border-weak-base": "#e2e3da",
|
||||
"border-weak-hover": "#d8d9d0",
|
||||
"border-weak-active": "#cfd0c7",
|
||||
"border-weak-selected": "#c4c6bc",
|
||||
"border-weak-disabled": "#eceee3",
|
||||
"border-weak-focus": "#c9cabf",
|
||||
"border-base": "#c4c6ba",
|
||||
"border-hover": "#b8baae",
|
||||
"border-active": "#abada3",
|
||||
"border-selected": "#979a90",
|
||||
"border-disabled": "#e5e7dd",
|
||||
"border-focus": "#b0b2a7",
|
||||
"border-strong-base": "#9fa293",
|
||||
"border-strong-hover": "#8e9185",
|
||||
"border-strong-active": "#7e8176",
|
||||
"border-strong-selected": "#6f7268",
|
||||
"border-strong-disabled": "#c7c9be",
|
||||
"border-strong-focus": "#878b7f",
|
||||
"surface-diff-add-base": "#e4f5e6",
|
||||
"surface-diff-delete-base": "#fae4eb",
|
||||
"surface-diff-hidden-base": "#dedfe9",
|
||||
"text-base": "#1f1f2f",
|
||||
"text-weak": "#52526b",
|
||||
"text-strong": "#05040c",
|
||||
"syntax-string": "#2fbf71",
|
||||
"syntax-primitive": "#d16090",
|
||||
"syntax-property": "#7c6bf5",
|
||||
"syntax-type": "#f7a14d",
|
||||
"syntax-constant": "#1d7fc5",
|
||||
"syntax-info": "#1d7fc5",
|
||||
"markdown-heading": "#7c6bf5",
|
||||
"markdown-text": "#1f1f2f",
|
||||
"markdown-link": "#7c6bf5",
|
||||
"markdown-link-text": "#1d7fc5",
|
||||
"markdown-code": "#2fbf71",
|
||||
"markdown-block-quote": "#f7a14d",
|
||||
"markdown-emph": "#f7a14d",
|
||||
"markdown-strong": "#d16090",
|
||||
"markdown-horizontal-rule": "#c3c5d4",
|
||||
"markdown-list-item": "#7c6bf5",
|
||||
"markdown-list-enumeration": "#1d7fc5",
|
||||
"markdown-image": "#7c6bf5",
|
||||
"markdown-image-text": "#1d7fc5",
|
||||
"markdown-code-block": "#1d7fc5"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#1d1e28",
|
||||
"primary": "#bd93f9",
|
||||
"success": "#50fa7b",
|
||||
"warning": "#ffb86c",
|
||||
"error": "#ff5555",
|
||||
"info": "#8be9fd",
|
||||
"interactive": "#bd93f9",
|
||||
"diffAdd": "#2fb27d",
|
||||
"diffDelete": "#ff6b81"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#14151f",
|
||||
"background-weak": "#181926",
|
||||
"background-strong": "#161722",
|
||||
"background-stronger": "#191a26",
|
||||
"border-weak-base": "#2d2f3c",
|
||||
"border-weak-hover": "#303244",
|
||||
"border-weak-active": "#35364c",
|
||||
"border-weak-selected": "#3b3d55",
|
||||
"border-weak-disabled": "#1e1f2b",
|
||||
"border-weak-focus": "#383a50",
|
||||
"border-base": "#3f415a",
|
||||
"border-hover": "#464967",
|
||||
"border-active": "#4d5073",
|
||||
"border-selected": "#55587f",
|
||||
"border-disabled": "#272834",
|
||||
"border-focus": "#4a4d6d",
|
||||
"border-strong-base": "#606488",
|
||||
"border-strong-hover": "#6a6e96",
|
||||
"border-strong-active": "#7378a3",
|
||||
"border-strong-selected": "#7d82b1",
|
||||
"border-strong-disabled": "#343649",
|
||||
"border-strong-focus": "#6f739c",
|
||||
"surface-diff-add-base": "#1f2a2f",
|
||||
"surface-diff-delete-base": "#2d1f27",
|
||||
"surface-diff-hidden-base": "#24253a",
|
||||
"text-base": "#f8f8f2",
|
||||
"text-weak": "#b6b9e4",
|
||||
"text-strong": "#ffffff",
|
||||
"syntax-string": "#50fa7b",
|
||||
"syntax-primitive": "#ff79c6",
|
||||
"syntax-property": "#bd93f9",
|
||||
"syntax-type": "#ffb86c",
|
||||
"syntax-constant": "#8be9fd",
|
||||
"syntax-info": "#8be9fd",
|
||||
"markdown-heading": "#bd93f9",
|
||||
"markdown-text": "#f8f8f2",
|
||||
"markdown-link": "#bd93f9",
|
||||
"markdown-link-text": "#8be9fd",
|
||||
"markdown-code": "#50fa7b",
|
||||
"markdown-block-quote": "#ffb86c",
|
||||
"markdown-emph": "#ffb86c",
|
||||
"markdown-strong": "#ff79c6",
|
||||
"markdown-horizontal-rule": "#44475a",
|
||||
"markdown-list-item": "#bd93f9",
|
||||
"markdown-list-enumeration": "#8be9fd",
|
||||
"markdown-image": "#bd93f9",
|
||||
"markdown-image-text": "#8be9fd",
|
||||
"markdown-code-block": "#f8f8f2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Monokai",
|
||||
"id": "monokai",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#fdf8ec",
|
||||
"primary": "#bf7bff",
|
||||
"success": "#4fb54b",
|
||||
"warning": "#f1a948",
|
||||
"error": "#e54b4b",
|
||||
"info": "#2d9ad7",
|
||||
"interactive": "#bf7bff",
|
||||
"diffAdd": "#bfe7a3",
|
||||
"diffDelete": "#f6a3ae"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#fdf8ec",
|
||||
"background-weak": "#f8f2e6",
|
||||
"background-strong": "#fbf5e8",
|
||||
"background-stronger": "#f7efdd",
|
||||
"border-weak-base": "#e9e0cf",
|
||||
"border-weak-hover": "#dfd5c3",
|
||||
"border-weak-active": "#d5cab7",
|
||||
"border-weak-selected": "#cabfad",
|
||||
"border-weak-disabled": "#f3ebdd",
|
||||
"border-weak-focus": "#d0c2b1",
|
||||
"border-base": "#c7b9a5",
|
||||
"border-hover": "#bcae98",
|
||||
"border-active": "#b0a28c",
|
||||
"border-selected": "#a49781",
|
||||
"border-disabled": "#efe5d6",
|
||||
"border-focus": "#b6a893",
|
||||
"border-strong-base": "#998b76",
|
||||
"border-strong-hover": "#8a7c67",
|
||||
"border-strong-active": "#7a6d58",
|
||||
"border-strong-selected": "#6c604c",
|
||||
"border-strong-disabled": "#d7cabc",
|
||||
"border-strong-focus": "#82745f",
|
||||
"surface-diff-add-base": "#e8f7e1",
|
||||
"surface-diff-delete-base": "#fde5e4",
|
||||
"surface-diff-hidden-base": "#e9e0d0",
|
||||
"text-base": "#292318",
|
||||
"text-weak": "#6d5c40",
|
||||
"text-strong": "#1c150c",
|
||||
"syntax-string": "#4fb54b",
|
||||
"syntax-primitive": "#d9487c",
|
||||
"syntax-property": "#bf7bff",
|
||||
"syntax-type": "#f1a948",
|
||||
"syntax-constant": "#2d9ad7",
|
||||
"syntax-info": "#2d9ad7",
|
||||
"markdown-heading": "#bf7bff",
|
||||
"markdown-text": "#292318",
|
||||
"markdown-link": "#bf7bff",
|
||||
"markdown-link-text": "#2d9ad7",
|
||||
"markdown-code": "#4fb54b",
|
||||
"markdown-block-quote": "#f1a948",
|
||||
"markdown-emph": "#f1a948",
|
||||
"markdown-strong": "#d9487c",
|
||||
"markdown-horizontal-rule": "#cdbdab",
|
||||
"markdown-list-item": "#bf7bff",
|
||||
"markdown-list-enumeration": "#2d9ad7",
|
||||
"markdown-image": "#bf7bff",
|
||||
"markdown-image-text": "#2d9ad7",
|
||||
"markdown-code-block": "#2d9ad7"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#272822",
|
||||
"primary": "#ae81ff",
|
||||
"success": "#a6e22e",
|
||||
"warning": "#fd971f",
|
||||
"error": "#f92672",
|
||||
"info": "#66d9ef",
|
||||
"interactive": "#ae81ff",
|
||||
"diffAdd": "#4d7f2a",
|
||||
"diffDelete": "#f4477c"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#23241e",
|
||||
"background-weak": "#27281f",
|
||||
"background-strong": "#25261f",
|
||||
"background-stronger": "#292a23",
|
||||
"border-weak-base": "#343528",
|
||||
"border-weak-hover": "#393a2d",
|
||||
"border-weak-active": "#3f4033",
|
||||
"border-weak-selected": "#454639",
|
||||
"border-weak-disabled": "#1d1e16",
|
||||
"border-weak-focus": "#414235",
|
||||
"border-base": "#494a3a",
|
||||
"border-hover": "#50523f",
|
||||
"border-active": "#585a45",
|
||||
"border-selected": "#60624b",
|
||||
"border-disabled": "#23241b",
|
||||
"border-focus": "#555741",
|
||||
"border-strong-base": "#6a6c55",
|
||||
"border-strong-hover": "#73755d",
|
||||
"border-strong-active": "#7d7f66",
|
||||
"border-strong-selected": "#878970",
|
||||
"border-strong-disabled": "#2c2d23",
|
||||
"border-strong-focus": "#7a7c63",
|
||||
"surface-diff-add-base": "#1e2a1d",
|
||||
"surface-diff-delete-base": "#301c24",
|
||||
"surface-diff-hidden-base": "#2f2f24",
|
||||
"text-base": "#f8f8f2",
|
||||
"text-weak": "#c5c5c0",
|
||||
"text-strong": "#ffffff",
|
||||
"syntax-string": "#a6e22e",
|
||||
"syntax-primitive": "#f92672",
|
||||
"syntax-property": "#ae81ff",
|
||||
"syntax-type": "#fd971f",
|
||||
"syntax-constant": "#66d9ef",
|
||||
"syntax-info": "#66d9ef",
|
||||
"markdown-heading": "#ae81ff",
|
||||
"markdown-text": "#f8f8f2",
|
||||
"markdown-link": "#ae81ff",
|
||||
"markdown-link-text": "#66d9ef",
|
||||
"markdown-code": "#a6e22e",
|
||||
"markdown-block-quote": "#fd971f",
|
||||
"markdown-emph": "#fd971f",
|
||||
"markdown-strong": "#f92672",
|
||||
"markdown-horizontal-rule": "#3b3c34",
|
||||
"markdown-list-item": "#ae81ff",
|
||||
"markdown-list-enumeration": "#66d9ef",
|
||||
"markdown-image": "#ae81ff",
|
||||
"markdown-image-text": "#66d9ef",
|
||||
"markdown-code-block": "#f8f8f2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Nord",
|
||||
"id": "nord",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#eceff4",
|
||||
"primary": "#5e81ac",
|
||||
"success": "#8fbcbb",
|
||||
"warning": "#d08770",
|
||||
"error": "#bf616a",
|
||||
"info": "#81a1c1",
|
||||
"interactive": "#5e81ac",
|
||||
"diffAdd": "#a3be8c",
|
||||
"diffDelete": "#bf616a"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#eceff4",
|
||||
"background-weak": "#e4e8f0",
|
||||
"background-strong": "#f1f3f8",
|
||||
"background-stronger": "#f6f8fc",
|
||||
"border-weak-base": "#d5dbe7",
|
||||
"border-weak-hover": "#c9d0de",
|
||||
"border-weak-active": "#bec5d4",
|
||||
"border-weak-selected": "#b2bacc",
|
||||
"border-weak-disabled": "#f0f3fa",
|
||||
"border-weak-focus": "#b9bfd0",
|
||||
"border-base": "#afb7cb",
|
||||
"border-hover": "#a3abc1",
|
||||
"border-active": "#979fb7",
|
||||
"border-selected": "#8b94ad",
|
||||
"border-disabled": "#e5e9f2",
|
||||
"border-focus": "#9ca4ba",
|
||||
"border-strong-base": "#757f97",
|
||||
"border-strong-hover": "#69718a",
|
||||
"border-strong-active": "#5d647d",
|
||||
"border-strong-selected": "#525970",
|
||||
"border-strong-disabled": "#c9cedc",
|
||||
"border-strong-focus": "#636c84",
|
||||
"surface-diff-add-base": "#e4f0e4",
|
||||
"surface-diff-delete-base": "#f4e1e4",
|
||||
"surface-diff-hidden-base": "#dfe6f2",
|
||||
"text-base": "#2e3440",
|
||||
"text-weak": "#4c566a",
|
||||
"text-strong": "#1f2530",
|
||||
"syntax-string": "#a3be8c",
|
||||
"syntax-primitive": "#bf616a",
|
||||
"syntax-property": "#5e81ac",
|
||||
"syntax-type": "#d08770",
|
||||
"syntax-constant": "#81a1c1",
|
||||
"syntax-info": "#81a1c1",
|
||||
"markdown-heading": "#5e81ac",
|
||||
"markdown-text": "#2e3440",
|
||||
"markdown-link": "#5e81ac",
|
||||
"markdown-link-text": "#81a1c1",
|
||||
"markdown-code": "#a3be8c",
|
||||
"markdown-block-quote": "#d08770",
|
||||
"markdown-emph": "#d08770",
|
||||
"markdown-strong": "#bf616a",
|
||||
"markdown-horizontal-rule": "#cbd3e1",
|
||||
"markdown-list-item": "#5e81ac",
|
||||
"markdown-list-enumeration": "#81a1c1",
|
||||
"markdown-image": "#5e81ac",
|
||||
"markdown-image-text": "#81a1c1",
|
||||
"markdown-code-block": "#5e81ac"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#2e3440",
|
||||
"primary": "#88c0d0",
|
||||
"success": "#a3be8c",
|
||||
"warning": "#d08770",
|
||||
"error": "#bf616a",
|
||||
"info": "#81a1c1",
|
||||
"interactive": "#88c0d0",
|
||||
"diffAdd": "#81a1c1",
|
||||
"diffDelete": "#bf616a"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#1f2430",
|
||||
"background-weak": "#222938",
|
||||
"background-strong": "#1c202a",
|
||||
"background-stronger": "#181c24",
|
||||
"border-weak-base": "#343a47",
|
||||
"border-weak-hover": "#383f50",
|
||||
"border-weak-active": "#3d4458",
|
||||
"border-weak-selected": "#434a62",
|
||||
"border-weak-disabled": "#151923",
|
||||
"border-weak-focus": "#3f4359",
|
||||
"border-base": "#4a5163",
|
||||
"border-hover": "#515870",
|
||||
"border-active": "#585f7c",
|
||||
"border-selected": "#606889",
|
||||
"border-disabled": "#1b202a",
|
||||
"border-focus": "#545b78",
|
||||
"border-strong-base": "#6a7492",
|
||||
"border-strong-hover": "#747e9f",
|
||||
"border-strong-active": "#7e88ac",
|
||||
"border-strong-selected": "#8993b9",
|
||||
"border-strong-disabled": "#232836",
|
||||
"border-strong-focus": "#76819f",
|
||||
"surface-diff-add-base": "#1f2e33",
|
||||
"surface-diff-delete-base": "#2e212a",
|
||||
"surface-diff-hidden-base": "#222b3a",
|
||||
"text-base": "#e5e9f0",
|
||||
"text-weak": "#a4adbf",
|
||||
"text-strong": "#f8fafc",
|
||||
"syntax-string": "#a3be8c",
|
||||
"syntax-primitive": "#d57780",
|
||||
"syntax-property": "#88c0d0",
|
||||
"syntax-type": "#eac196",
|
||||
"syntax-constant": "#81a1c1",
|
||||
"syntax-info": "#81a1c1",
|
||||
"markdown-heading": "#88c0d0",
|
||||
"markdown-text": "#e5e9f0",
|
||||
"markdown-link": "#88c0d0",
|
||||
"markdown-link-text": "#81a1c1",
|
||||
"markdown-code": "#a3be8c",
|
||||
"markdown-block-quote": "#d08770",
|
||||
"markdown-emph": "#d08770",
|
||||
"markdown-strong": "#bf616a",
|
||||
"markdown-horizontal-rule": "#2f384a",
|
||||
"markdown-list-item": "#88c0d0",
|
||||
"markdown-list-enumeration": "#81a1c1",
|
||||
"markdown-image": "#88c0d0",
|
||||
"markdown-image-text": "#81a1c1",
|
||||
"markdown-code-block": "#cbd3e1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "OC-1",
|
||||
"id": "oc-1",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#8e8b8b",
|
||||
"primary": "#dcde8d",
|
||||
"success": "#12c905",
|
||||
"warning": "#ffdc17",
|
||||
"error": "#fc533a",
|
||||
"info": "#a753ae",
|
||||
"interactive": "#034cff",
|
||||
"diffAdd": "#9ff29a",
|
||||
"diffDelete": "#fc533a"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#f8f7f7",
|
||||
"background-weak": "var(--smoke-light-3)",
|
||||
"background-strong": "var(--smoke-light-1)",
|
||||
"background-stronger": "#fcfcfc",
|
||||
"surface-base": "var(--smoke-light-alpha-2)",
|
||||
"base": "var(--smoke-light-alpha-2)",
|
||||
"surface-base-hover": "#0500000f",
|
||||
"surface-base-active": "var(--smoke-light-alpha-3)",
|
||||
"surface-base-interactive-active": "var(--cobalt-light-alpha-3)",
|
||||
"base2": "var(--smoke-light-alpha-2)",
|
||||
"base3": "var(--smoke-light-alpha-2)",
|
||||
"surface-inset-base": "var(--smoke-light-alpha-2)",
|
||||
"surface-inset-base-hover": "var(--smoke-light-alpha-3)",
|
||||
"surface-inset-strong": "#1f000017",
|
||||
"surface-inset-strong-hover": "#1f000017",
|
||||
"surface-raised-base": "var(--smoke-light-alpha-1)",
|
||||
"surface-float-base": "var(--smoke-dark-1)",
|
||||
"surface-float-base-hover": "var(--smoke-dark-2)",
|
||||
"surface-raised-base-hover": "var(--smoke-light-alpha-2)",
|
||||
"surface-raised-base-active": "var(--smoke-light-alpha-3)",
|
||||
"surface-raised-strong": "var(--smoke-light-1)",
|
||||
"surface-raised-strong-hover": "var(--white)",
|
||||
"surface-raised-stronger": "var(--white)",
|
||||
"surface-raised-stronger-hover": "var(--white)",
|
||||
"surface-weak": "var(--smoke-light-alpha-3)",
|
||||
"surface-weaker": "var(--smoke-light-alpha-4)",
|
||||
"surface-strong": "#ffffff",
|
||||
"surface-raised-stronger-non-alpha": "var(--white)",
|
||||
"surface-brand-base": "var(--yuzu-light-9)",
|
||||
"surface-brand-hover": "var(--yuzu-light-10)",
|
||||
"surface-interactive-base": "var(--cobalt-light-3)",
|
||||
"surface-interactive-hover": "var(--cobalt-light-4)",
|
||||
"surface-interactive-weak": "var(--cobalt-light-2)",
|
||||
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
|
||||
"surface-success-base": "var(--apple-light-3)",
|
||||
"surface-success-weak": "var(--apple-light-2)",
|
||||
"surface-success-strong": "var(--apple-light-9)",
|
||||
"surface-warning-base": "var(--solaris-light-3)",
|
||||
"surface-warning-weak": "var(--solaris-light-2)",
|
||||
"surface-warning-strong": "var(--solaris-light-9)",
|
||||
"surface-critical-base": "var(--ember-light-3)",
|
||||
"surface-critical-weak": "var(--ember-light-2)",
|
||||
"surface-critical-strong": "var(--ember-light-9)",
|
||||
"surface-info-base": "var(--lilac-light-3)",
|
||||
"surface-info-weak": "var(--lilac-light-2)",
|
||||
"surface-info-strong": "var(--lilac-light-9)",
|
||||
"surface-diff-unchanged-base": "#ffffff00",
|
||||
"surface-diff-skip-base": "var(--smoke-light-2)",
|
||||
"surface-diff-hidden-base": "var(--blue-light-3)",
|
||||
"surface-diff-hidden-weak": "var(--blue-light-2)",
|
||||
"surface-diff-hidden-weaker": "var(--blue-light-1)",
|
||||
"surface-diff-hidden-strong": "var(--blue-light-5)",
|
||||
"surface-diff-hidden-stronger": "var(--blue-light-9)",
|
||||
"surface-diff-add-base": "#dafbe0",
|
||||
"surface-diff-add-weak": "var(--mint-light-2)",
|
||||
"surface-diff-add-weaker": "var(--mint-light-1)",
|
||||
"surface-diff-add-strong": "var(--mint-light-5)",
|
||||
"surface-diff-add-stronger": "var(--mint-light-9)",
|
||||
"surface-diff-delete-base": "var(--ember-light-3)",
|
||||
"surface-diff-delete-weak": "var(--ember-light-2)",
|
||||
"surface-diff-delete-weaker": "var(--ember-light-1)",
|
||||
"surface-diff-delete-strong": "var(--ember-light-6)",
|
||||
"surface-diff-delete-stronger": "var(--ember-light-9)",
|
||||
"input-base": "var(--smoke-light-1)",
|
||||
"input-hover": "var(--smoke-light-2)",
|
||||
"input-active": "var(--cobalt-light-1)",
|
||||
"input-selected": "var(--cobalt-light-4)",
|
||||
"input-focus": "var(--cobalt-light-1)",
|
||||
"input-disabled": "var(--smoke-light-4)",
|
||||
"text-base": "var(--smoke-light-11)",
|
||||
"text-weak": "var(--smoke-light-9)",
|
||||
"text-weaker": "var(--smoke-light-8)",
|
||||
"text-strong": "var(--smoke-light-12)",
|
||||
"text-invert-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-invert-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-invert-weaker": "var(--smoke-dark-alpha-8)",
|
||||
"text-invert-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-interactive-base": "var(--cobalt-light-9)",
|
||||
"text-on-brand-base": "var(--smoke-light-alpha-11)",
|
||||
"text-on-interactive-base": "var(--smoke-light-1)",
|
||||
"text-on-interactive-weak": "var(--smoke-dark-alpha-11)",
|
||||
"text-on-success-base": "var(--apple-light-10)",
|
||||
"text-on-critical-base": "var(--ember-light-10)",
|
||||
"text-on-critical-weak": "var(--ember-light-8)",
|
||||
"text-on-critical-strong": "var(--ember-light-12)",
|
||||
"text-on-warning-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-on-info-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-diff-add-base": "var(--mint-light-11)",
|
||||
"text-diff-delete-base": "var(--ember-light-10)",
|
||||
"text-diff-delete-strong": "var(--ember-light-12)",
|
||||
"text-diff-add-strong": "var(--mint-light-12)",
|
||||
"text-on-info-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-on-info-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-on-warning-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-on-warning-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-on-success-weak": "var(--apple-light-6)",
|
||||
"text-on-success-strong": "var(--apple-light-12)",
|
||||
"text-on-brand-weak": "var(--smoke-light-alpha-9)",
|
||||
"text-on-brand-weaker": "var(--smoke-light-alpha-8)",
|
||||
"text-on-brand-strong": "var(--smoke-light-alpha-12)",
|
||||
"button-secondary-base": "#fdfcfc",
|
||||
"button-secondary-hover": "#faf9f9",
|
||||
"border-base": "var(--smoke-light-alpha-7)",
|
||||
"border-hover": "var(--smoke-light-alpha-8)",
|
||||
"border-active": "var(--smoke-light-alpha-9)",
|
||||
"border-selected": "var(--cobalt-light-alpha-9)",
|
||||
"border-disabled": "var(--smoke-light-alpha-8)",
|
||||
"border-focus": "var(--smoke-light-alpha-9)",
|
||||
"border-weak-base": "var(--smoke-light-alpha-5)",
|
||||
"border-strong-base": "var(--smoke-light-alpha-7)",
|
||||
"border-strong-hover": "var(--smoke-light-alpha-8)",
|
||||
"border-strong-active": "var(--smoke-light-alpha-7)",
|
||||
"border-strong-selected": "var(--cobalt-light-alpha-6)",
|
||||
"border-strong-disabled": "var(--smoke-light-alpha-6)",
|
||||
"border-strong-focus": "var(--smoke-light-alpha-7)",
|
||||
"border-weak-hover": "var(--smoke-light-alpha-6)",
|
||||
"border-weak-active": "var(--smoke-light-alpha-7)",
|
||||
"border-weak-selected": "var(--cobalt-light-alpha-5)",
|
||||
"border-weak-disabled": "var(--smoke-light-alpha-6)",
|
||||
"border-weak-focus": "var(--smoke-light-alpha-7)",
|
||||
"border-interactive-base": "var(--cobalt-light-7)",
|
||||
"border-interactive-hover": "var(--cobalt-light-8)",
|
||||
"border-interactive-active": "var(--cobalt-light-9)",
|
||||
"border-interactive-selected": "var(--cobalt-light-9)",
|
||||
"border-interactive-disabled": "var(--smoke-light-8)",
|
||||
"border-interactive-focus": "var(--cobalt-light-9)",
|
||||
"border-success-base": "var(--apple-light-6)",
|
||||
"border-success-hover": "var(--apple-light-7)",
|
||||
"border-success-selected": "var(--apple-light-9)",
|
||||
"border-warning-base": "var(--solaris-light-6)",
|
||||
"border-warning-hover": "var(--solaris-light-7)",
|
||||
"border-warning-selected": "var(--solaris-light-9)",
|
||||
"border-critical-base": "var(--ember-light-6)",
|
||||
"border-critical-hover": "var(--ember-light-7)",
|
||||
"border-critical-selected": "var(--ember-light-9)",
|
||||
"border-info-base": "var(--lilac-light-6)",
|
||||
"border-info-hover": "var(--lilac-light-7)",
|
||||
"border-info-selected": "var(--lilac-light-9)",
|
||||
"icon-base": "var(--smoke-light-9)",
|
||||
"icon-hover": "var(--smoke-light-11)",
|
||||
"icon-active": "var(--smoke-light-12)",
|
||||
"icon-selected": "var(--smoke-light-12)",
|
||||
"icon-disabled": "var(--smoke-light-8)",
|
||||
"icon-focus": "var(--smoke-light-12)",
|
||||
"icon-invert-base": "#ffffff",
|
||||
"icon-weak-base": "var(--smoke-light-7)",
|
||||
"icon-weak-hover": "var(--smoke-light-8)",
|
||||
"icon-weak-active": "var(--smoke-light-9)",
|
||||
"icon-weak-selected": "var(--smoke-light-10)",
|
||||
"icon-weak-disabled": "var(--smoke-light-6)",
|
||||
"icon-weak-focus": "var(--smoke-light-9)",
|
||||
"icon-strong-base": "var(--smoke-light-12)",
|
||||
"icon-strong-hover": "#151313",
|
||||
"icon-strong-active": "#020202",
|
||||
"icon-strong-selected": "#020202",
|
||||
"icon-strong-disabled": "var(--smoke-light-8)",
|
||||
"icon-strong-focus": "#020202",
|
||||
"icon-brand-base": "var(--smoke-light-12)",
|
||||
"icon-interactive-base": "var(--cobalt-light-9)",
|
||||
"icon-success-base": "var(--apple-light-7)",
|
||||
"icon-success-hover": "var(--apple-light-8)",
|
||||
"icon-success-active": "var(--apple-light-11)",
|
||||
"icon-warning-base": "var(--amber-light-7)",
|
||||
"icon-warning-hover": "var(--amber-light-8)",
|
||||
"icon-warning-active": "var(--amber-light-11)",
|
||||
"icon-critical-base": "var(--ember-light-10)",
|
||||
"icon-critical-hover": "var(--ember-light-11)",
|
||||
"icon-critical-active": "var(--ember-light-12)",
|
||||
"icon-info-base": "var(--lilac-light-7)",
|
||||
"icon-info-hover": "var(--lilac-light-8)",
|
||||
"icon-info-active": "var(--lilac-light-11)",
|
||||
"icon-on-brand-base": "var(--smoke-light-alpha-11)",
|
||||
"icon-on-brand-hover": "var(--smoke-light-alpha-12)",
|
||||
"icon-on-brand-selected": "var(--smoke-light-alpha-12)",
|
||||
"icon-on-interactive-base": "var(--smoke-light-1)",
|
||||
"icon-agent-plan-base": "var(--purple-light-9)",
|
||||
"icon-agent-docs-base": "var(--amber-light-9)",
|
||||
"icon-agent-ask-base": "var(--cyan-light-9)",
|
||||
"icon-agent-build-base": "var(--cobalt-light-9)",
|
||||
"icon-on-success-base": "var(--apple-light-alpha-9)",
|
||||
"icon-on-success-hover": "var(--apple-light-alpha-10)",
|
||||
"icon-on-success-selected": "var(--apple-light-alpha-11)",
|
||||
"icon-on-warning-base": "var(--amber-lightalpha-9)",
|
||||
"icon-on-warning-hover": "var(--amber-lightalpha-10)",
|
||||
"icon-on-warning-selected": "var(--amber-lightalpha-11)",
|
||||
"icon-on-critical-base": "var(--ember-light-alpha-9)",
|
||||
"icon-on-critical-hover": "var(--ember-light-alpha-10)",
|
||||
"icon-on-critical-selected": "var(--ember-light-alpha-11)",
|
||||
"icon-on-info-base": "var(--lilac-light-9)",
|
||||
"icon-on-info-hover": "var(--lilac-light-alpha-10)",
|
||||
"icon-on-info-selected": "var(--lilac-light-alpha-11)",
|
||||
"icon-diff-add-base": "var(--mint-light-11)",
|
||||
"icon-diff-add-hover": "var(--mint-light-12)",
|
||||
"icon-diff-add-active": "var(--mint-light-12)",
|
||||
"icon-diff-delete-base": "var(--ember-light-10)",
|
||||
"icon-diff-delete-hover": "var(--ember-light-11)",
|
||||
"syntax-comment": "var(--text-weak)",
|
||||
"syntax-regexp": "var(--text-base)",
|
||||
"syntax-string": "#006656",
|
||||
"syntax-keyword": "var(--text-weak)",
|
||||
"syntax-primitive": "#fb4804",
|
||||
"syntax-operator": "var(--text-base)",
|
||||
"syntax-variable": "var(--text-strong)",
|
||||
"syntax-property": "#ed6dc8",
|
||||
"syntax-type": "#596600",
|
||||
"syntax-constant": "#007b80",
|
||||
"syntax-punctuation": "var(--text-base)",
|
||||
"syntax-object": "var(--text-strong)",
|
||||
"syntax-success": "var(--apple-light-10)",
|
||||
"syntax-warning": "var(--amber-light-10)",
|
||||
"syntax-critical": "var(--ember-light-10)",
|
||||
"syntax-info": "#0092a8",
|
||||
"syntax-diff-add": "var(--mint-light-11)",
|
||||
"syntax-diff-delete": "var(--ember-light-11)",
|
||||
"syntax-diff-unknown": "#ff0000",
|
||||
"markdown-heading": "#d68c27",
|
||||
"markdown-text": "#1a1a1a",
|
||||
"markdown-link": "#3b7dd8",
|
||||
"markdown-link-text": "#318795",
|
||||
"markdown-code": "#3d9a57",
|
||||
"markdown-block-quote": "#b0851f",
|
||||
"markdown-emph": "#b0851f",
|
||||
"markdown-strong": "#d68c27",
|
||||
"markdown-horizontal-rule": "#8a8a8a",
|
||||
"markdown-list-item": "#3b7dd8",
|
||||
"markdown-list-enumeration": "#318795",
|
||||
"markdown-image": "#3b7dd8",
|
||||
"markdown-image-text": "#318795",
|
||||
"markdown-code-block": "#1a1a1a",
|
||||
"border-color": "#ffffff",
|
||||
"border-weaker-base": "var(--smoke-light-alpha-3)",
|
||||
"border-weaker-hover": "var(--smoke-light-alpha-4)",
|
||||
"border-weaker-active": "var(--smoke-light-alpha-6)",
|
||||
"border-weaker-selected": "var(--cobalt-light-alpha-4)",
|
||||
"border-weaker-disabled": "var(--smoke-light-alpha-2)",
|
||||
"border-weaker-focus": "var(--smoke-light-alpha-6)",
|
||||
"button-ghost-hover": "var(--smoke-light-alpha-2)",
|
||||
"button-ghost-hover2": "var(--smoke-light-alpha-3)",
|
||||
"avatar-background-pink": "#feeef8",
|
||||
"avatar-background-mint": "#e1fbf4",
|
||||
"avatar-background-orange": "#fff1e7",
|
||||
"avatar-background-purple": "#f9f1fe",
|
||||
"avatar-background-cyan": "#e7f9fb",
|
||||
"avatar-background-lime": "#eefadc",
|
||||
"avatar-text-pink": "#cd1d8d",
|
||||
"avatar-text-mint": "#147d6f",
|
||||
"avatar-text-orange": "#ed5f00",
|
||||
"avatar-text-purple": "#8445bc",
|
||||
"avatar-text-cyan": "#0894b3",
|
||||
"avatar-text-lime": "#5d770d"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#716c6b",
|
||||
"primary": "#fab283",
|
||||
"success": "#12c905",
|
||||
"warning": "#fcd53a",
|
||||
"error": "#fc533a",
|
||||
"info": "#edb2f1",
|
||||
"interactive": "#034cff",
|
||||
"diffAdd": "#c8ffc4",
|
||||
"diffDelete": "#fc533a"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "var(--smoke-dark-1)",
|
||||
"background-weak": "#1c1717",
|
||||
"background-strong": "#151313",
|
||||
"background-stronger": "#191515",
|
||||
"surface-base": "var(--smoke-dark-alpha-2)",
|
||||
"base": "var(--smoke-dark-alpha-2)",
|
||||
"surface-base-hover": "#e0b7b716",
|
||||
"surface-base-active": "var(--smoke-dark-alpha-3)",
|
||||
"surface-base-interactive-active": "var(--cobalt-dark-alpha-2)",
|
||||
"base2": "var(--smoke-dark-alpha-2)",
|
||||
"base3": "var(--smoke-dark-alpha-2)",
|
||||
"surface-inset-base": "#0e0b0b7f",
|
||||
"surface-inset-base-hover": "#0e0b0b7f",
|
||||
"surface-inset-strong": "#060505cc",
|
||||
"surface-inset-strong-hover": "#060505cc",
|
||||
"surface-raised-base": "var(--smoke-dark-alpha-3)",
|
||||
"surface-float-base": "var(--smoke-dark-1)",
|
||||
"surface-float-base-hover": "var(--smoke-dark-2)",
|
||||
"surface-raised-base-hover": "var(--smoke-dark-alpha-4)",
|
||||
"surface-raised-base-active": "var(--smoke-dark-alpha-5)",
|
||||
"surface-raised-strong": "var(--smoke-dark-alpha-4)",
|
||||
"surface-raised-strong-hover": "var(--smoke-dark-alpha-6)",
|
||||
"surface-raised-stronger": "var(--smoke-dark-alpha-6)",
|
||||
"surface-raised-stronger-hover": "var(--smoke-dark-alpha-7)",
|
||||
"surface-weak": "var(--smoke-dark-alpha-4)",
|
||||
"surface-weaker": "var(--smoke-dark-alpha-5)",
|
||||
"surface-strong": "var(--smoke-dark-alpha-7)",
|
||||
"surface-raised-stronger-non-alpha": "var(--smoke-dark-3)",
|
||||
"surface-brand-base": "var(--yuzu-light-9)",
|
||||
"surface-brand-hover": "var(--yuzu-light-10)",
|
||||
"surface-interactive-base": "var(--cobalt-light-3)",
|
||||
"surface-interactive-hover": "var(--cobalt-light-4)",
|
||||
"surface-interactive-weak": "var(--cobalt-light-2)",
|
||||
"surface-interactive-weak-hover": "var(--cobalt-light-3)",
|
||||
"surface-success-base": "var(--apple-light-3)",
|
||||
"surface-success-weak": "var(--apple-light-2)",
|
||||
"surface-success-strong": "var(--apple-light-9)",
|
||||
"surface-warning-base": "var(--solaris-light-3)",
|
||||
"surface-warning-weak": "var(--solaris-light-2)",
|
||||
"surface-warning-strong": "var(--solaris-light-9)",
|
||||
"surface-critical-base": "var(--ember-dark-3)",
|
||||
"surface-critical-weak": "var(--ember-dark-2)",
|
||||
"surface-critical-strong": "var(--ember-dark-9)",
|
||||
"surface-info-base": "var(--lilac-light-3)",
|
||||
"surface-info-weak": "var(--lilac-light-2)",
|
||||
"surface-info-strong": "var(--lilac-light-9)",
|
||||
"surface-diff-unchanged-base": "var(--smoke-dark-1)",
|
||||
"surface-diff-skip-base": "var(--smoke-dark-alpha-1)",
|
||||
"surface-diff-hidden-base": "var(--blue-dark-2)",
|
||||
"surface-diff-hidden-weak": "var(--blue-dark-1)",
|
||||
"surface-diff-hidden-weaker": "var(--blue-dark-3)",
|
||||
"surface-diff-hidden-strong": "var(--blue-dark-5)",
|
||||
"surface-diff-hidden-stronger": "var(--blue-dark-11)",
|
||||
"surface-diff-add-base": "var(--mint-dark-3)",
|
||||
"surface-diff-add-weak": "var(--mint-dark-4)",
|
||||
"surface-diff-add-weaker": "var(--mint-dark-3)",
|
||||
"surface-diff-add-strong": "var(--mint-dark-5)",
|
||||
"surface-diff-add-stronger": "var(--mint-dark-11)",
|
||||
"surface-diff-delete-base": "var(--ember-dark-3)",
|
||||
"surface-diff-delete-weak": "var(--ember-dark-4)",
|
||||
"surface-diff-delete-weaker": "var(--ember-dark-3)",
|
||||
"surface-diff-delete-strong": "var(--ember-dark-5)",
|
||||
"surface-diff-delete-stronger": "var(--ember-dark-11)",
|
||||
"input-base": "var(--smoke-dark-2)",
|
||||
"input-hover": "var(--smoke-dark-2)",
|
||||
"input-active": "var(--cobalt-dark-1)",
|
||||
"input-selected": "var(--cobalt-dark-2)",
|
||||
"input-focus": "var(--cobalt-dark-1)",
|
||||
"input-disabled": "var(--smoke-dark-4)",
|
||||
"text-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-weaker": "var(--smoke-dark-alpha-8)",
|
||||
"text-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-invert-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-invert-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-invert-weaker": "var(--smoke-dark-alpha-8)",
|
||||
"text-invert-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-interactive-base": "var(--cobalt-dark-11)",
|
||||
"text-on-brand-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-on-interactive-base": "var(--smoke-dark-12)",
|
||||
"text-on-interactive-weak": "var(--smoke-dark-alpha-11)",
|
||||
"text-on-success-base": "var(--apple-dark-9)",
|
||||
"text-on-critical-base": "var(--ember-dark-9)",
|
||||
"text-on-critical-weak": "var(--ember-dark-8)",
|
||||
"text-on-critical-strong": "var(--ember-dark-12)",
|
||||
"text-on-warning-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-on-info-base": "var(--smoke-dark-alpha-11)",
|
||||
"text-diff-add-base": "var(--mint-dark-11)",
|
||||
"text-diff-delete-base": "var(--ember-dark-9)",
|
||||
"text-diff-delete-strong": "var(--ember-dark-12)",
|
||||
"text-diff-add-strong": "var(--mint-dark-8)",
|
||||
"text-on-info-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-on-info-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-on-warning-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-on-warning-strong": "var(--smoke-dark-alpha-12)",
|
||||
"text-on-success-weak": "var(--apple-dark-8)",
|
||||
"text-on-success-strong": "var(--apple-dark-12)",
|
||||
"text-on-brand-weak": "var(--smoke-dark-alpha-9)",
|
||||
"text-on-brand-weaker": "var(--smoke-dark-alpha-8)",
|
||||
"text-on-brand-strong": "var(--smoke-dark-alpha-12)",
|
||||
"button-secondary-base": "#231f1f",
|
||||
"button-secondary-hover": "#2a2727",
|
||||
"border-base": "var(--smoke-dark-alpha-7)",
|
||||
"border-hover": "var(--smoke-dark-alpha-8)",
|
||||
"border-active": "var(--smoke-dark-alpha-9)",
|
||||
"border-selected": "var(--cobalt-dark-alpha-11)",
|
||||
"border-disabled": "var(--smoke-dark-alpha-8)",
|
||||
"border-focus": "var(--smoke-dark-alpha-9)",
|
||||
"border-weak-base": "var(--smoke-dark-alpha-6)",
|
||||
"border-strong-base": "var(--smoke-dark-alpha-8)",
|
||||
"border-strong-hover": "var(--smoke-dark-alpha-7)",
|
||||
"border-strong-active": "var(--smoke-dark-alpha-8)",
|
||||
"border-strong-selected": "var(--cobalt-dark-alpha-6)",
|
||||
"border-strong-disabled": "var(--smoke-dark-alpha-6)",
|
||||
"border-strong-focus": "var(--smoke-dark-alpha-8)",
|
||||
"border-weak-hover": "var(--smoke-dark-alpha-7)",
|
||||
"border-weak-active": "var(--smoke-dark-alpha-8)",
|
||||
"border-weak-selected": "var(--cobalt-dark-alpha-6)",
|
||||
"border-weak-disabled": "var(--smoke-dark-alpha-6)",
|
||||
"border-weak-focus": "var(--smoke-dark-alpha-8)",
|
||||
"border-interactive-base": "var(--cobalt-light-7)",
|
||||
"border-interactive-hover": "var(--cobalt-light-8)",
|
||||
"border-interactive-active": "var(--cobalt-light-9)",
|
||||
"border-interactive-selected": "var(--cobalt-light-9)",
|
||||
"border-interactive-disabled": "var(--smoke-light-8)",
|
||||
"border-interactive-focus": "var(--cobalt-light-9)",
|
||||
"border-success-base": "var(--apple-light-6)",
|
||||
"border-success-hover": "var(--apple-light-7)",
|
||||
"border-success-selected": "var(--apple-light-9)",
|
||||
"border-warning-base": "var(--solaris-light-6)",
|
||||
"border-warning-hover": "var(--solaris-light-7)",
|
||||
"border-warning-selected": "var(--solaris-light-9)",
|
||||
"border-critical-base": "var(--ember-dark-5)",
|
||||
"border-critical-hover": "var(--ember-dark-7)",
|
||||
"border-critical-selected": "var(--ember-dark-9)",
|
||||
"border-info-base": "var(--lilac-light-6)",
|
||||
"border-info-hover": "var(--lilac-light-7)",
|
||||
"border-info-selected": "var(--lilac-light-9)",
|
||||
"icon-base": "var(--smoke-dark-9)",
|
||||
"icon-hover": "var(--smoke-dark-10)",
|
||||
"icon-active": "var(--smoke-dark-11)",
|
||||
"icon-selected": "var(--smoke-dark-12)",
|
||||
"icon-disabled": "var(--smoke-dark-7)",
|
||||
"icon-focus": "var(--smoke-dark-12)",
|
||||
"icon-invert-base": "var(--smoke-dark-1)",
|
||||
"icon-weak-base": "var(--smoke-dark-6)",
|
||||
"icon-weak-hover": "var(--smoke-light-7)",
|
||||
"icon-weak-active": "var(--smoke-light-8)",
|
||||
"icon-weak-selected": "var(--smoke-light-9)",
|
||||
"icon-weak-disabled": "var(--smoke-light-4)",
|
||||
"icon-weak-focus": "var(--smoke-light-9)",
|
||||
"icon-strong-base": "var(--smoke-dark-12)",
|
||||
"icon-strong-hover": "#f6f3f3",
|
||||
"icon-strong-active": "#fcfcfc",
|
||||
"icon-strong-selected": "#fdfcfc",
|
||||
"icon-strong-disabled": "var(--smoke-dark-8)",
|
||||
"icon-strong-focus": "#fdfcfc",
|
||||
"icon-brand-base": "var(--white)",
|
||||
"icon-interactive-base": "var(--cobalt-dark-9)",
|
||||
"icon-success-base": "var(--apple-dark-7)",
|
||||
"icon-success-hover": "var(--apple-dark-8)",
|
||||
"icon-success-active": "var(--apple-dark-11)",
|
||||
"icon-warning-base": "var(--amber-dark-7)",
|
||||
"icon-warning-hover": "var(--amber-dark-8)",
|
||||
"icon-warning-active": "var(--amber-dark-11)",
|
||||
"icon-critical-base": "var(--ember-dark-9)",
|
||||
"icon-critical-hover": "var(--ember-dark-11)",
|
||||
"icon-critical-active": "var(--ember-dark-12)",
|
||||
"icon-info-base": "var(--lilac-dark-7)",
|
||||
"icon-info-hover": "var(--lilac-dark-8)",
|
||||
"icon-info-active": "var(--lilac-dark-11)",
|
||||
"icon-on-brand-base": "var(--smoke-light-alpha-11)",
|
||||
"icon-on-brand-hover": "var(--smoke-light-alpha-12)",
|
||||
"icon-on-brand-selected": "var(--smoke-light-alpha-12)",
|
||||
"icon-on-interactive-base": "var(--smoke-dark-12)",
|
||||
"icon-agent-plan-base": "var(--purple-dark-9)",
|
||||
"icon-agent-docs-base": "var(--amber-dark-9)",
|
||||
"icon-agent-ask-base": "var(--cyan-dark-9)",
|
||||
"icon-agent-build-base": "var(--cobalt-dark-11)",
|
||||
"icon-on-success-base": "var(--apple-dark-alpha-9)",
|
||||
"icon-on-success-hover": "var(--apple-dark-alpha-10)",
|
||||
"icon-on-success-selected": "var(--apple-dark-alpha-11)",
|
||||
"icon-on-warning-base": "var(--amber-darkalpha-9)",
|
||||
"icon-on-warning-hover": "var(--amber-darkalpha-10)",
|
||||
"icon-on-warning-selected": "var(--amber-darkalpha-11)",
|
||||
"icon-on-critical-base": "var(--ember-dark-alpha-9)",
|
||||
"icon-on-critical-hover": "var(--ember-dark-alpha-10)",
|
||||
"icon-on-critical-selected": "var(--ember-dark-alpha-11)",
|
||||
"icon-on-info-base": "var(--lilac-dark-9)",
|
||||
"icon-on-info-hover": "var(--lilac-dark-alpha-10)",
|
||||
"icon-on-info-selected": "var(--lilac-dark-alpha-11)",
|
||||
"icon-diff-add-base": "var(--mint-dark-11)",
|
||||
"icon-diff-add-hover": "var(--mint-dark-10)",
|
||||
"icon-diff-add-active": "var(--mint-dark-11)",
|
||||
"icon-diff-delete-base": "var(--ember-dark-9)",
|
||||
"icon-diff-delete-hover": "var(--ember-dark-10)",
|
||||
"syntax-comment": "var(--text-weak)",
|
||||
"syntax-regexp": "var(--text-base)",
|
||||
"syntax-string": "#00ceb9",
|
||||
"syntax-keyword": "var(--text-weak)",
|
||||
"syntax-primitive": "#ffba92",
|
||||
"syntax-operator": "var(--text-weak)",
|
||||
"syntax-variable": "var(--text-strong)",
|
||||
"syntax-property": "#ff9ae2",
|
||||
"syntax-type": "#ecf58c",
|
||||
"syntax-constant": "#93e9f6",
|
||||
"syntax-punctuation": "var(--text-weak)",
|
||||
"syntax-object": "var(--text-strong)",
|
||||
"syntax-success": "var(--apple-dark-10)",
|
||||
"syntax-warning": "var(--amber-dark-10)",
|
||||
"syntax-critical": "var(--ember-dark-10)",
|
||||
"syntax-info": "#93e9f6",
|
||||
"syntax-diff-add": "var(--mint-dark-11)",
|
||||
"syntax-diff-delete": "var(--ember-dark-11)",
|
||||
"syntax-diff-unknown": "#ff0000",
|
||||
"markdown-heading": "#9d7cd8",
|
||||
"markdown-text": "#eeeeee",
|
||||
"markdown-link": "#fab283",
|
||||
"markdown-link-text": "#56b6c2",
|
||||
"markdown-code": "#7fd88f",
|
||||
"markdown-block-quote": "#e5c07b",
|
||||
"markdown-emph": "#e5c07b",
|
||||
"markdown-strong": "#f5a742",
|
||||
"markdown-horizontal-rule": "#808080",
|
||||
"markdown-list-item": "#fab283",
|
||||
"markdown-list-enumeration": "#56b6c2",
|
||||
"markdown-image": "#fab283",
|
||||
"markdown-image-text": "#56b6c2",
|
||||
"markdown-code-block": "#eeeeee",
|
||||
"border-color": "#ffffff",
|
||||
"border-weaker-base": "var(--smoke-dark-alpha-3)",
|
||||
"border-weaker-hover": "var(--smoke-dark-alpha-4)",
|
||||
"border-weaker-active": "var(--smoke-dark-alpha-6)",
|
||||
"border-weaker-selected": "var(--cobalt-dark-alpha-3)",
|
||||
"border-weaker-disabled": "var(--smoke-dark-alpha-2)",
|
||||
"border-weaker-focus": "var(--smoke-dark-alpha-6)",
|
||||
"button-ghost-hover": "var(--smoke-dark-alpha-2)",
|
||||
"button-ghost-hover2": "var(--smoke-dark-alpha-3)",
|
||||
"avatar-background-pink": "#501b3f",
|
||||
"avatar-background-mint": "#033a34",
|
||||
"avatar-background-orange": "#5f2a06",
|
||||
"avatar-background-purple": "#432155",
|
||||
"avatar-background-cyan": "#0f3058",
|
||||
"avatar-background-lime": "#2b3711",
|
||||
"avatar-text-pink": "#e34ba9",
|
||||
"avatar-text-mint": "#95f3d9",
|
||||
"avatar-text-orange": "#ff802b",
|
||||
"avatar-text-purple": "#9d5bd2",
|
||||
"avatar-text-cyan": "#369eff",
|
||||
"avatar-text-lime": "#c4f042"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "One Dark Pro",
|
||||
"id": "onedarkpro",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#f5f6f8",
|
||||
"primary": "#528bff",
|
||||
"success": "#4fa66d",
|
||||
"warning": "#d19a66",
|
||||
"error": "#e06c75",
|
||||
"info": "#61afef",
|
||||
"interactive": "#528bff",
|
||||
"diffAdd": "#c2ebcf",
|
||||
"diffDelete": "#f7c1c5"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#f5f6f8",
|
||||
"background-weak": "#eef0f4",
|
||||
"background-strong": "#fafbfc",
|
||||
"background-stronger": "#ffffff",
|
||||
"border-weak-base": "#dee2eb",
|
||||
"border-weak-hover": "#d4d9e3",
|
||||
"border-weak-active": "#caced6",
|
||||
"border-weak-selected": "#bec4d0",
|
||||
"border-weak-disabled": "#f4f6fb",
|
||||
"border-weak-focus": "#c4cada",
|
||||
"border-base": "#b5bccd",
|
||||
"border-hover": "#aab1c2",
|
||||
"border-active": "#a0a7b8",
|
||||
"border-selected": "#959cae",
|
||||
"border-disabled": "#eceef4",
|
||||
"border-focus": "#a6adbf",
|
||||
"border-strong-base": "#747c92",
|
||||
"border-strong-hover": "#6a7287",
|
||||
"border-strong-active": "#60687c",
|
||||
"border-strong-selected": "#565e71",
|
||||
"border-strong-disabled": "#cbd0dd",
|
||||
"border-strong-focus": "#666d82",
|
||||
"surface-diff-add-base": "#e5f4ea",
|
||||
"surface-diff-delete-base": "#fde7ea",
|
||||
"surface-diff-hidden-base": "#e4e8f4",
|
||||
"text-base": "#2b303b",
|
||||
"text-weak": "#6b717f",
|
||||
"text-strong": "#0e1118",
|
||||
"syntax-string": "#4fa66d",
|
||||
"syntax-primitive": "#d85462",
|
||||
"syntax-property": "#528bff",
|
||||
"syntax-type": "#d19a66",
|
||||
"syntax-constant": "#61afef",
|
||||
"syntax-info": "#61afef",
|
||||
"markdown-heading": "#528bff",
|
||||
"markdown-text": "#2b303b",
|
||||
"markdown-link": "#528bff",
|
||||
"markdown-link-text": "#61afef",
|
||||
"markdown-code": "#4fa66d",
|
||||
"markdown-block-quote": "#d19a66",
|
||||
"markdown-emph": "#d19a66",
|
||||
"markdown-strong": "#d85462",
|
||||
"markdown-horizontal-rule": "#d3d7e4",
|
||||
"markdown-list-item": "#528bff",
|
||||
"markdown-list-enumeration": "#61afef",
|
||||
"markdown-image": "#528bff",
|
||||
"markdown-image-text": "#61afef",
|
||||
"markdown-code-block": "#528bff"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#1e222a",
|
||||
"primary": "#61afef",
|
||||
"success": "#98c379",
|
||||
"warning": "#e5c07b",
|
||||
"error": "#e06c75",
|
||||
"info": "#56b6c2",
|
||||
"interactive": "#61afef",
|
||||
"diffAdd": "#4b815a",
|
||||
"diffDelete": "#b2555f"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#1e222a",
|
||||
"background-weak": "#212631",
|
||||
"background-strong": "#1b1f27",
|
||||
"background-stronger": "#171b23",
|
||||
"border-weak-base": "#323848",
|
||||
"border-weak-hover": "#363d52",
|
||||
"border-weak-active": "#3c435c",
|
||||
"border-weak-selected": "#424967",
|
||||
"border-weak-disabled": "#141720",
|
||||
"border-weak-focus": "#3f4560",
|
||||
"border-base": "#4a5164",
|
||||
"border-hover": "#515871",
|
||||
"border-active": "#585f7e",
|
||||
"border-selected": "#60688a",
|
||||
"border-disabled": "#1a1e27",
|
||||
"border-focus": "#555c79",
|
||||
"border-strong-base": "#6a7390",
|
||||
"border-strong-hover": "#737c9d",
|
||||
"border-strong-active": "#7d87ab",
|
||||
"border-strong-selected": "#8791b8",
|
||||
"border-strong-disabled": "#212533",
|
||||
"border-strong-focus": "#7680a2",
|
||||
"surface-diff-add-base": "#1c2a26",
|
||||
"surface-diff-delete-base": "#2a1c22",
|
||||
"surface-diff-hidden-base": "#232836",
|
||||
"text-base": "#abb2bf",
|
||||
"text-weak": "#818899",
|
||||
"text-strong": "#f6f7fb",
|
||||
"syntax-string": "#98c379",
|
||||
"syntax-primitive": "#e06c75",
|
||||
"syntax-property": "#61afef",
|
||||
"syntax-type": "#e5c07b",
|
||||
"syntax-constant": "#56b6c2",
|
||||
"syntax-info": "#56b6c2",
|
||||
"markdown-heading": "#61afef",
|
||||
"markdown-text": "#abb2bf",
|
||||
"markdown-link": "#61afef",
|
||||
"markdown-link-text": "#56b6c2",
|
||||
"markdown-code": "#98c379",
|
||||
"markdown-block-quote": "#e5c07b",
|
||||
"markdown-emph": "#e5c07b",
|
||||
"markdown-strong": "#e06c75",
|
||||
"markdown-horizontal-rule": "#2d3444",
|
||||
"markdown-list-item": "#61afef",
|
||||
"markdown-list-enumeration": "#56b6c2",
|
||||
"markdown-image": "#61afef",
|
||||
"markdown-image-text": "#56b6c2",
|
||||
"markdown-code-block": "#abb2bf"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user