Compare commits

...

91 Commits

Author SHA1 Message Date
opencode 9721223b7e release: v1.0.208 2025-12-29 20:44:22 +00:00
Adam 35a626e711 fix(desktop): don't flash permissions with auto-accept 2025-12-29 14:40:53 -06:00
Adam bb7b0ff221 fix(desktop): scroll sync 2025-12-29 14:36:27 -06:00
Adam 68b4038196 fix(desktop): more performance/scrolling fixes 2025-12-29 14:23:41 -06:00
Adam 3109214900 feat(desktop): auto-accept edits toggle 2025-12-29 14:23:41 -06:00
Adam 86ccc3409b fix(desktop): toast position 2025-12-29 14:23:41 -06:00
Github Action a89089c88f Update Nix flake.lock and hashes 2025-12-29 20:06:20 +00:00
CasualDeveloper e617c5d689 fix: prevent truncated Claude streams (#6388) 2025-12-29 14:04:53 -06:00
Frank 31983ca5ff zen: do not switch provider for models require stick provider 2025-12-29 14:27:51 -05:00
Aiden Cline 59e3b7409f chore: fix type error 2025-12-29 12:38:35 -06:00
Daniel Polito b7ce46f7a1 Desktop: Image Preview and Dedupe File Upload (#6372) 2025-12-29 11:22:48 -06:00
Brett Heap 82b8d8fa5d fix(tui): make auth URLs clickable regardless of line wrapping (#6317)
Co-authored-by: brettheap <brett.heap@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-12-29 11:21:09 -06:00
Adam 77c837eb1a fix(desktop): throttle markdown renders 2025-12-29 11:19:40 -06:00
Adam db77cc9845 chore: cleanup 2025-12-29 11:19:40 -06:00
Aiden Cline 68043edae6 ci: changelog script update (#6371) 2025-12-29 11:12:25 -06:00
Frainer Encarnación 337681dbbf fix(lsp): ESLint LSP server fails to auto-install on Windows (#6366) 2025-12-29 11:02:46 -06:00
Adam 66afc034d1 fix(desktop): don't show summary when already complete 2025-12-29 10:58:53 -06:00
Adam 11ab8de59f fix(desktop): markdown lists 2025-12-29 10:47:05 -06:00
Adam 5f074edc3a fix(desktop): performance/jankiness 2025-12-29 10:42:48 -06:00
Matt Silverlock 56b5cdf883 feat: install local plugin dependencies from package.json (#6302)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-29 10:37:41 -06:00
Adam fb0e1e4d8d Revert "fix(desktop): jankiness"
This reverts commit 831e9bce51.
2025-12-29 09:56:33 -06:00
Github Action b745b1593f Update Nix flake.lock and hashes 2025-12-29 15:55:48 +00:00
Adam 7376c3f8e7 feat(desktop): latex support 2025-12-29 09:54:22 -06:00
Adam 831e9bce51 fix(desktop): jankiness 2025-12-29 09:47:57 -06:00
Adam 5de73abd82 fix(desktop): markdown styles 2025-12-29 09:47:57 -06:00
Zeno Jiricek 3adbbc1b23 docs: add opencode-skillful plugin to ecosystem page (#6333) 2025-12-29 09:32:44 -06:00
Frank c6c29b3dcf zen: minimax m2.1 2025-12-29 10:16:32 -05:00
Adam a687d7c15f fix(desktop): one permission at a time 2025-12-29 09:07:36 -06:00
Daniel Polito 0c6da69f39 Desktop: Edit Project (#6360) 2025-12-29 08:54:49 -06:00
Adam c4930eb6b2 fix(desktop): more fine-grained state updates for permissions 2025-12-29 08:47:38 -06:00
Frank a24549fce7 docs: update MiniMax console link in integration instructions 2025-12-29 09:29:01 -05:00
Adam c0f9b13630 fix(desktop): more fine-grained state updates 2025-12-29 08:21:32 -06:00
Adam 98fd53fd5f fix(core): preserve imperative statements in summary 2025-12-29 07:25:55 -06:00
Adam 5b02a3029e fix(desktop): max height on edit tool calls 2025-12-29 07:03:44 -06:00
Frank 94e851c2a2 docs: add MiniMax integration instructions to providers documentation 2025-12-29 07:45:54 -05:00
GitHub Action 1658a3ff59 ignore: update download stats 2025-12-29 2025-12-29 12:05:06 +00:00
Adam 9c8bc64138 fix(desktop): sync last agent and model when changing session 2025-12-29 02:57:28 -06:00
Adam 80f704ebbf fix(desktop): context usage alignment 2025-12-29 02:47:51 -06:00
Matt Silverlock 4dae6d1fcf meta: use colors for agents (#5845) 2025-12-28 23:05:17 -06:00
Matt Silverlock 5d2cab39da docs: add compaction, watcher, experimental and provider options (#6304)
Co-authored-by: OpenCode <opencode@example.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-28 23:01:43 -06:00
GitHub Action 6963f96d4b chore: generate 2025-12-29 04:56:54 +00:00
Alice Alexandra Moore 05a9e7ce7a docs: clarify that MCP tools require glob patterns to disable (#6306)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-28 22:56:22 -06:00
GitHub Action 896d18ab3f chore: generate 2025-12-29 04:44:48 +00:00
Grégoire Morpain 893888536a fix(bedrock): support region and bearer token configuration (#6332)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-28 22:44:15 -06:00
GitHub Action c6221fc8b3 chore: generate 2025-12-29 04:39:43 +00:00
Connor Adams ae67f43ff0 feat: add support for .claude/skills directory (#6252) 2025-12-28 22:39:10 -06:00
opencode 76880dce0d release: v1.0.207 2025-12-29 01:57:53 +00:00
Adam aafffb5b4b chore: cleanup 2025-12-28 19:54:22 -06:00
Adam a71c9e3f2e fix(desktop): edit diffs 2025-12-28 19:49:39 -06:00
Adam 0156f03e0e chore: cleanup theme stuff 2025-12-28 19:27:36 -06:00
Frank e0bb96a9f9 wip: bench 2025-12-28 20:00:49 -05:00
Daniel Polito 82e5d6d458 Desktop: Sync LSP updates (#6305) 2025-12-28 16:07:36 -06:00
Adam a4411c21b6 feat(desktop): theme preview 2025-12-28 15:47:05 -06:00
Frank 9d61370ac4 sync 2025-12-28 15:33:18 -05:00
Frank f3febd6e39 wip: benchmark 2025-12-28 14:55:05 -05:00
GitHub Action f12d55bf1e chore: generate 2025-12-28 19:13:43 +00:00
Matt Silverlock 0c19b71f42 docs: add plugin configuration documentation (#6301)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-28 13:13:11 -06:00
Mohak S 70fa66397e docs: add opencode-notifier plugin to ecosystem (#6283) 2025-12-28 13:09:38 -06:00
Daniel Polito 6e8cd3174c Include current working directory in local MCP transport (#6303) 2025-12-28 13:09:24 -06:00
GitHub Action 5bfffbe083 chore: generate 2025-12-28 19:06:59 +00:00
Didier Durand 29d8557d41 doc: fix typos in various files (#6294) 2025-12-28 13:06:25 -06:00
Didier Durand ffd20b4477 chore: activate code coverage in bun test config (#6297) 2025-12-28 19:05:55 +00:00
opencode 2abaa46e23 release: v1.0.206 2025-12-28 19:05:54 +00:00
GitHub Action 0cbbb20d22 chore: generate 2025-12-28 18:54:55 +00:00
Frank 81c5e7b9ed wip: benchmark 2025-12-28 13:54:11 -05:00
opencode ddf4897eaa release: v1.0.205 2025-12-28 18:37:58 +00:00
Adam 040939fb72 chore: cleanup theme stuff 2025-12-28 10:21:32 -06:00
Adam f89b83a6d7 chore: cleanup theme stuff 2025-12-28 10:14:30 -06:00
Adam 82a876da4d chore: cleanup 2025-12-28 06:41:59 -06:00
GitHub Action 69a15ae9c1 ignore: update download stats 2025-12-28 2025-12-28 12:04:31 +00:00
Adam 18c8e5f451 chore: cleanup 2025-12-28 05:47:22 -06:00
Adam ba3a1cfa0b chore: cleanup 2025-12-28 05:47:21 -06:00
Github Action d8563160f7 Update Nix flake.lock and hashes 2025-12-28 11:13:54 +00:00
Adam 4a9ff9412e feat(desktop): themes 2025-12-28 05:12:36 -06:00
Matt Silverlock d6db6ff198 fix: handle non-text response parts in GitHub action (#6173) 2025-12-27 21:24:10 -06:00
Aiden Cline 79c263494f tweak: inform agent if no skills are available 2025-12-27 21:20:00 -06:00
Adam 1b5bf32ce5 chore: permissions ux 2025-12-27 20:40:25 -06:00
Adam 2e972b3fdc fix(desktop): copy/paste in terminal 2025-12-27 20:18:59 -06:00
Adam d70e9fb01e chore(desktop): cleanup 2025-12-27 19:59:16 -06:00
Adam fc082a0f14 fix(desktop): drag file over entire body to attach 2025-12-27 19:49:35 -06:00
Adam 953e4e9446 chore(desktop): vertical tabs 2025-12-27 19:43:52 -06:00
rektide 7ea0d37ee3 Thinking & tool call visibility settings for /copy and /export (#6243)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-27 19:32:33 -06:00
scarf e35d97f9d7 feat: add bash shell completions (#6239) 2025-12-27 19:14:56 -06:00
GitHub Action 2c0d9a46cb chore: generate 2025-12-28 01:12:02 +00:00
Nindaleth 2fe7a7f2d3 docs: document attach command (#6254)
Co-authored-by: Black_Fox <radekliska@gmail.com>
2025-12-27 19:11:30 -06:00
Connor Adams 8a2f4ddf70 chore: update INVALID_DIRS to include plural 'skills' directory (#6255) 2025-12-27 19:10:51 -06:00
processtrader 7a94d7a2c5 fix: stats command to correctly handle --days 0 for current day statistics (#6259)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-27 19:10:23 -06:00
Aiden Cline de28fafb47 fix: search all recent models instead of only top 5 in TUI /models command 2025-12-27 19:07:38 -06:00
Ivan Pantic 9d485dd307 docs: add opencode-notificator to ecosystem plugins list (#6269)
Co-authored-by: Ivan Pantic <panta@talentkit.io>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 18:54:27 -06:00
GitHub Action 613813ac12 chore: generate 2025-12-28 00:53:48 +00:00
ewired 7617f59441 Allow line numbers and ranges in autocomplete (#4238) 2025-12-27 18:53:17 -06:00
116 changed files with 7329 additions and 1221 deletions
+1
View File
@@ -1,5 +1,6 @@
---
description: ALWAYS use this when writing docs
color: "#38A3EE"
---
You are an expert technical documentation writer
+1
View File
@@ -2,6 +2,7 @@
mode: primary
hidden: true
model: opencode/claude-haiku-4-5
color: "#44BA81"
tools:
"*": false
"github-triage": true
+2
View File
@@ -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) |
+32 -35
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg="
"nodeModules": "sha256-2i/QMBzp9MalOXur36mXaDDU8R9G/0dODCODEQOnaCU="
}
+31 -6
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.204",
"version": "1.0.208",
"description": "",
"type": "module",
"exports": {
+45 -42
View File
@@ -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>
)
}
+12 -9
View File
@@ -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"
+95 -17
View File
@@ -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,
+25 -6
View File
@@ -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">
+55 -39
View File
@@ -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)
},
},
}
}
+1
View File
@@ -70,6 +70,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
{
...project,
...(metadata ?? {}),
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
},
]
}
+130
View File
@@ -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)
},
}
},
})
+44 -20
View File
@@ -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),
+12 -9
View File
@@ -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>
+177 -58
View File
@@ -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>
+84 -19
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
+1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+31 -6
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.204",
"version": "1.0.208",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -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 -1
View File
@@ -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
View File
@@ -2,3 +2,5 @@ preload = ["@opentui/solid/preload"]
[test]
preload = ["./test/preload.ts"]
# Enable code coverage
coverage = true
+3 -3
View File
@@ -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
+57 -3
View File
@@ -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() {
+14 -5
View File
@@ -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>
)
}
+6 -2
View File
@@ -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")
+1
View File
@@ -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)
+4 -3
View File
@@ -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,
+3
View File
@@ -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 = {
+24 -9
View File
@@ -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 users 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, workspacerelative, a/ or b/ diff prefixes, or bare filename/suffix.
* Line/column (1based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
* Do not use URIs like file://, vscode://, or https://.
-1
View File
@@ -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"
+39 -24
View File
@@ -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
})
+1 -1
View File
@@ -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.
+18 -13
View File
@@ -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")
},
})
})
+26 -26
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+12 -6
View File
@@ -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>
)
}
+37 -26
View File
@@ -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}>
+18 -8
View File
@@ -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 */
+20 -4
View File
@@ -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;
}
+125 -37
View File
@@ -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"] {
+144 -63
View File
@@ -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>
+66
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[data-component="toast-region"] {
position: fixed;
bottom: 32px;
bottom: 48px;
right: 32px;
z-index: 1000;
display: flex;
+1 -4
View File
@@ -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}
+4
View File
@@ -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: [] })
+84 -3
View File
@@ -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,
}
}
+9 -1
View File
@@ -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,
})
+2
View File
@@ -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);
+2 -477
View File
@@ -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);
}
}
+194
View File
@@ -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})`
}
+180
View File
@@ -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)
}
},
}
},
})
+35
View File
@@ -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"
}
}
}
}
}
}
+44
View File
@@ -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"
+103
View File
@@ -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)
}
}
+325
View File
@@ -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 ")
}
+131
View File
@@ -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"
}
}
}
+131
View File
@@ -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"
}
}
}
+131
View File
@@ -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"
}
}
}
+131
View File
@@ -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"
}
}
}
+535
View File
@@ -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