Compare commits

..

61 Commits

Author SHA1 Message Date
exe.dev user 11b14dff4d fix(desktop): load WSL shell environment 2026-07-22 04:14:47 +00:00
opencode-agent[bot] c9db6e9a1f fix(app): show running shell command (#38080)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-22 11:47:42 +10:00
opencode-agent[bot] 130038eb63 fix(app): defer unavailable notification state (#38186)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-22 11:16:51 +10:00
Frank 4438f69aac zen: add security check 2026-07-21 14:51:43 -04:00
opencode-agent[bot] 76ced5418f chore: generate 2026-07-21 18:43:01 +00:00
Frank c40e3e74a7 zen: add security check 2026-07-21 14:34:00 -04:00
Frank 0317531906 zen: add laguna-2-2.1 2026-07-21 12:59:49 -04:00
opencode-agent[bot] 5f241f1cc1 chore: generate 2026-07-21 15:19:02 +00:00
Daniel Polito b513fafe83 fix(app): support nested slash command autocomplete (#38097) 2026-07-21 23:08:23 +08:00
Daniel Polito 21c4c93770 fix(ui): improve progress circle contrast (#38101) 2026-07-21 23:08:09 +08:00
Frank abc4b83f58 Zen: gemini 3.6 flash and 3.5 flash lite 2026-07-21 10:59:30 -04:00
opencode-agent[bot] cb562b2c62 chore: generate 2026-07-21 09:58:21 +00:00
Aarav Sareen 2d2339d37d feat(app): draggable project rows (#38055) 2026-07-21 17:56:42 +08:00
Aiden Cline 849c2598ab docs: generalize session title model description (#37986) 2026-07-20 14:20:13 -05:00
Aiden Cline 3033afba51 fix(provider): normalize Mistral family tool call IDs (#37982)
Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com>
2026-07-20 13:58:49 -05:00
Matthias Reso 5a8ee27254 fix(provider): update muse-spark reasoning default and system prompt (#37562)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-07-20 13:15:42 -05:00
OpeOginni 4cc022481c fix(opencode): display proper session import errors (#36258) 2026-07-20 11:43:27 -05:00
opencode d36a2d8981 sync release versions for v1.18.4 2026-07-20 15:28:16 +00:00
opencode-agent[bot] 96ff82a11f fix(console): remove desktop promo overlay
Requested by: @thdxr (dax via Slack)
2026-07-20 15:28:00 +00:00
opencode-agent[bot] 4872c48c23 fix(app): complete mentions at cursor (#37941)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-20 22:53:10 +08:00
opencode-agent[bot] 47fc6f2991 fix(app): preserve mentions during paste (#37940)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-20 22:52:14 +08:00
opencode-agent[bot] da312b009c fix(app): preserve command menu drafts (#37942)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-20 22:49:39 +08:00
opencode-agent[bot] 70535a1bad chore: update nix node_modules hashes 2026-07-20 14:35:56 +00:00
Luke Parker 43e472bba7 feat(app): sync embedded terminal theme (#37931) 2026-07-20 14:22:26 +00:00
opencode-agent[bot] b67fda133a chore: update nix node_modules hashes 2026-07-20 13:24:20 +00:00
Luke Parker e2531dff0f chore(app): bump ghostty-web (#37923)
Includes the physical-pixel rendering and native Ghostty sprite work from anomalyco/ghostty-web#3.
2026-07-20 13:09:46 +00:00
opencode-agent[bot] cc3615b535 fix(app): tolerate missing message parents (#37918)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-20 12:20:41 +00:00
Luke Parker 5542415b64 fix(app): shrink Windows drawer close button (#37909) 2026-07-20 11:21:22 +00:00
Luke Parker 8bec40a816 fix(app): align Windows upgrade drawer (#37895) 2026-07-20 10:32:38 +00:00
opencode-agent[bot] 4a81e8392b fix(app): gate notification server selection (#37890)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-20 09:22:18 +00:00
opencode-agent[bot] a19b52e85b fix(app): omit empty prompt text parts (#37577)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
2026-07-20 17:09:06 +10:00
opencode-agent[bot] 9105f35966 chore: generate 2026-07-20 06:50:36 +00:00
Aarav Sareen 3ba3954aa0 feat(app): review panel improvements (#36716)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-20 06:49:23 +00:00
Aarav Sareen 3fc5af6dd1 feat(app): update settings description line height (#37759) 2026-07-20 15:57:11 +10:00
opencode-agent[bot] 5586f9675e fix(app): restore model variant accessibility (#37857)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-20 05:20:56 +00:00
opencode-agent[bot] 3b719aba48 chore: generate 2026-07-20 04:55:12 +00:00
Qiping Li 20a3a2138e feat(opencode): use adaptive thinking effort for kimi family on anthr… (#37696)
Co-authored-by: liqiping <liqiping@msh.team>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-07-19 23:54:00 -05:00
Frank cd46f22d51 sync 2026-07-19 23:55:16 -04:00
Frank cfa134f042 Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-07-19 23:30:38 -04:00
Frank 61653a20a5 sync 2026-07-19 23:30:36 -04:00
opencode-agent[bot] 2a097f3af7 fix(llm): expand context overflow patterns (#37840)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-19 22:15:21 -05:00
David Hill ba4b8e21f4 feat(app): toggle debug tools from dev badge (#36689)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-20 12:22:48 +10:00
Rahul A Mistry 7985c2066a fix(app): show keybind tooltips on prompt input controls (#37824) 2026-07-20 10:22:38 +10:00
opencode-agent[bot] 67caf894e0 fix(opencode): increase OpenAI header timeout (#37770)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-19 10:41:48 -05:00
opencode-agent[bot] 78587c141b fix(app): restore question pager segments (#37575)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
2026-07-19 13:00:57 +00:00
opencode-agent[bot] f5573281ca chore: generate 2026-07-19 07:46:36 +00:00
opencode-agent[bot] 4817228f96 docs(go): update Kimi K3 request estimates (#37725)
Co-authored-by: Frank <frank@anoma.ly>
2026-07-19 03:45:27 -04:00
Frank 5a5117b36c Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-07-19 02:50:38 -04:00
Frank 6a288426b8 go: support openai route 2026-07-19 02:50:36 -04:00
usrnk1 573ac85983 feat(desktop): align new session project picker (#37461)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-19 16:15:54 +10:00
Frank 1221bbbe14 Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-07-19 02:08:19 -04:00
Frank 6bea0f4fa4 zen: grok 4.5 adjusted price 2026-07-19 02:08:05 -04:00
usrnk1 cc34084dfe feat(desktop): prevent overlapping composer borders (#37490) 2026-07-19 16:01:16 +10:00
opencode-agent[bot] b8142c7aa8 chore: update nix node_modules hashes 2026-07-18 17:54:17 +00:00
Simon Klee ba3f966ed0 build: bump opentui to 0.4.5 (#37616) 2026-07-18 19:40:12 +02:00
Jérôme Benoit b95fe7b2d7 fix(nix): restore desktop integration (#37197)
Co-authored-by: Test User <test@test.com>
2026-07-18 10:41:44 -05:00
Jérôme Benoit 9da9ed9de6 fix(nix): relax Bun version check for desktop build (#36767)
Co-authored-by: Test User <test@test.com>
2026-07-18 10:41:15 -05:00
opencode-agent[bot] fab2133129 chore: configure trust center domain (#36910)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-18 02:51:31 -04:00
opencode-agent[bot] 901c9e7329 fix(app): disable unavailable file navigation (#37595)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-18 15:39:15 +10:00
opencode-agent[bot] 3476e6baa5 chore: update nix node_modules hashes 2026-07-18 03:05:17 +00:00
opencode-agent[bot] 86e04d4174 revert(tui): downgrade opentui to 0.4.3 (#37582)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-17 21:51:48 -05:00
175 changed files with 5885 additions and 1727 deletions
+30 -30
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@corvu/drawer": "catalog:", "@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0", "@dnd-kit/abstract": "0.5.0",
@@ -65,7 +65,7 @@
"diff": "catalog:", "diff": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"marked-shiki": "catalog:", "marked-shiki": "catalog:",
@@ -95,7 +95,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.18.3", "version": "1.18.4",
"bin": { "bin": {
"lildax": "./bin/lildax.cjs", "lildax": "./bin/lildax.cjs",
}, },
@@ -143,7 +143,7 @@
}, },
"packages/codemode": { "packages/codemode": {
"name": "@opencode-ai/codemode", "name": "@opencode-ai/codemode",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"acorn": "8.15.0", "acorn": "8.15.0",
"effect": "catalog:", "effect": "catalog:",
@@ -157,7 +157,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -193,7 +193,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -220,7 +220,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.82", "@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -242,7 +242,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -266,7 +266,7 @@
}, },
"packages/console/support": { "packages/console/support": {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-core": "workspace:*",
@@ -286,7 +286,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.18.3", "version": "1.18.4",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -380,7 +380,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"effect": "catalog:", "effect": "catalog:",
@@ -434,7 +434,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -448,7 +448,7 @@
}, },
"packages/effect-sqlite-node": { "packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
}, },
@@ -460,7 +460,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@hono/standard-validator": "catalog:", "@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -492,7 +492,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -508,7 +508,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83",
@@ -539,7 +539,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
@@ -558,7 +558,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.18.3", "version": "1.18.4",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -689,7 +689,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -765,7 +765,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -780,7 +780,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@opencode-ai/server", "name": "@opencode-ai/server",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*", "@opencode-ai/protocol": "workspace:*",
@@ -795,7 +795,7 @@
}, },
"packages/session-ui": { "packages/session-ui": {
"name": "@opencode-ai/session-ui", "name": "@opencode-ai/session-ui",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -839,7 +839,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -852,7 +852,7 @@
}, },
"packages/stats/app": { "packages/stats/app": {
"name": "@opencode-ai/stats-app", "name": "@opencode-ai/stats-app",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
@@ -886,7 +886,7 @@
}, },
"packages/stats/core": { "packages/stats/core": {
"name": "@opencode-ai/stats-core", "name": "@opencode-ai/stats-core",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@aws-sdk/client-athena": "3.933.0", "@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0", "@planetscale/database": "1.19.0",
@@ -905,7 +905,7 @@
}, },
"packages/stats/server": { "packages/stats/server": {
"name": "@opencode-ai/stats-server", "name": "@opencode-ai/stats-server",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@aws-sdk/client-firehose": "3.933.0", "@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -947,7 +947,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@opencode-ai/tui", "name": "@opencode-ai/tui",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
@@ -974,7 +974,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
@@ -1025,7 +1025,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -3845,7 +3845,7 @@
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#513463a", {}, "anomalyco-ghostty-web-513463a", "sha512-GZR8LSmgGzViWnBJrqRI8MpAZRCJxhcr1Hi9Tyeh7YRooHZQjK9J97FQRD3tbBaM2wjq05gzGY2UEsG+JtZeBw=="], "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
+19
View File
@@ -8,6 +8,25 @@ export const zoneID = "430ba34c138cfb5360826c4909f99be8"
export const awsStage = $app.stage === "production" ? "production" : "dev" export const awsStage = $app.stage === "production" ? "production" : "dev"
export const deployAws = $app.stage === awsStage export const deployAws = $app.stage === awsStage
if ($app.stage === "production") {
new cloudflare.DnsRecord("TrustCenter", {
zoneId: zoneID,
name: "trust.opencode.ai",
type: "CNAME",
content: "3a69a5bb27875189.vercel-dns-016.com",
proxied: false,
ttl: 60,
})
new cloudflare.DnsRecord("TrustCenterVerification", {
zoneId: zoneID,
name: "opencode.ai",
type: "TXT",
content: "compai-domain-verification=org_6993a99c6200a2d642bb115d",
ttl: 60,
})
}
new cloudflare.RegionalHostname("RegionalHostname", { new cloudflare.RegionalHostname("RegionalHostname", {
hostname: domain, hostname: domain,
regionKey: "us", regionKey: "us",
+66 -33
View File
@@ -8,6 +8,8 @@
makeWrapper, makeWrapper,
writableTmpDirAsHomeHook, writableTmpDirAsHomeHook,
autoPatchelfHook, autoPatchelfHook,
copyDesktopItems,
makeDesktopItem,
opencode, opencode,
}: }:
let let
@@ -27,9 +29,12 @@ stdenv.mkDerivation (finalAttrs: {
nodejs nodejs
makeWrapper makeWrapper
writableTmpDirAsHomeHook writableTmpDirAsHomeHook
] ++ lib.optionals stdenv.hostPlatform.isLinux [ ]
++ lib.optionals stdenv.hostPlatform.isLinux [
autoPatchelfHook autoPatchelfHook
] ++ lib.optionals stdenv.hostPlatform.isDarwin [ copyDesktopItems
]
++ lib.optionals stdenv.hostPlatform.isDarwin [
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing. # Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
darwin.autoSignDarwinBinariesHook darwin.autoSignDarwinBinariesHook
]; ];
@@ -38,20 +43,37 @@ stdenv.mkDerivation (finalAttrs: {
(lib.getLib stdenv.cc.cc) (lib.getLib stdenv.cc.cc)
]; ];
desktopItems = lib.optional stdenv.hostPlatform.isLinux (makeDesktopItem {
name = "ai.opencode.desktop";
desktopName = "OpenCode";
exec = "opencode-desktop %U";
icon = "ai.opencode.desktop";
# Electron 41 derives X11 WM_CLASS from app.name.
startupWMClass = "OpenCode";
categories = [ "Development" ];
});
env = opencode.env // { env = opencode.env // {
ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; ELECTRON_SKIP_BINARY_DOWNLOAD = "1";
}; };
# https://github.com/electron/electron/issues/31121 postPatch =
# mac builds use a .app bundle which doesnt have this issue # NOTE: Relax Bun version check to be a warning instead of an error
postPatch = lib.optionalString stdenv.isLinux '' ''
BASE_PATH=packages/desktop substituteInPlace packages/script/src/index.ts \
FILES=(src/main/windows.ts) --replace-fail 'throw new Error(`This script requires bun@''${expectedBunVersionRange}' \
for file in "''${FILES[@]}"; do 'console.warn(`Warning: This script requires bun@''${expectedBunVersionRange}'
substituteInPlace $BASE_PATH/$file \ ''
--replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'" # https://github.com/electron/electron/issues/31121
done # mac builds use a .app bundle which doesnt have this issue
''; + lib.optionalString stdenv.isLinux ''
BASE_PATH=packages/desktop
FILES=(src/main/windows.ts)
for file in "''${FILES[@]}"; do
substituteInPlace $BASE_PATH/$file \
--replace-fail "process.resourcesPath" "'$out/opt/opencode-desktop/resources'"
done
'';
preBuild = '' preBuild = ''
cp -r "${electron.dist}" $HOME/.electron-dist cp -r "${electron.dist}" $HOME/.electron-dist
@@ -76,27 +98,38 @@ stdenv.mkDerivation (finalAttrs: {
runHook postBuild runHook postBuild
''; '';
installPhase = installPhase = ''
'' runHook preInstall
runHook preInstall ''
'' + lib.optionalString stdenv.hostPlatform.isDarwin ''
+ lib.optionalString stdenv.hostPlatform.isDarwin '' mkdir -p $out/Applications
mkdir -p $out/Applications mv dist/mac*/*.app $out/Applications
mv dist/mac*/*.app $out/Applications makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop
makeWrapper "$out/Applications/OpenCode.app/Contents/MacOS/OpenCode" $out/bin/opencode-desktop ''
'' + lib.optionalString stdenv.hostPlatform.isLinux ''
+ lib.optionalString stdenv.hostPlatform.isLinux '' mkdir -p $out/opt/opencode-desktop
mkdir -p $out/opt/opencode-desktop cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop
cp -r dist/linux*-unpacked/{resources,LICENSE*} $out/opt/opencode-desktop install -Dm644 resources/icons/32x32.png \
makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \ "$out/share/icons/hicolor/32x32/apps/ai.opencode.desktop.png"
--inherit-argv0 \ install -Dm644 resources/icons/64x64.png \
--set ELECTRON_FORCE_IS_PACKAGED 1 \ "$out/share/icons/hicolor/64x64/apps/ai.opencode.desktop.png"
--add-flags $out/opt/opencode-desktop/resources/app.asar \ install -Dm644 resources/icons/128x128.png \
--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" "$out/share/icons/hicolor/128x128/apps/ai.opencode.desktop.png"
'' install -Dm644 resources/icons/128x128@2x.png \
+ '' "$out/share/icons/hicolor/256x256/apps/ai.opencode.desktop.png"
runHook postInstall install -Dm644 resources/icons/icon.png \
''; "$out/share/icons/hicolor/512x512/apps/ai.opencode.desktop.png"
install -Dm644 resources/ai.opencode.desktop.metainfo.xml \
"$out/share/metainfo/ai.opencode.desktop.metainfo.xml"
makeWrapper ${lib.getExe electron} $out/bin/opencode-desktop \
--inherit-argv0 \
--set ELECTRON_FORCE_IS_PACKAGED 1 \
--add-flags $out/opt/opencode-desktop/resources/app.asar \
--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}"
''
+ ''
runHook postInstall
'';
autoPatchelfIgnoreMissingDeps = [ autoPatchelfIgnoreMissingDeps = [
"libc.musl-x86_64.so.1" "libc.musl-x86_64.so.1"
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-zAftZieWR0hWQQVkhEa826iueHaRnKxtyRodhNb+Apc=", "x86_64-linux": "sha256-qt11SKmOjq0KU542QFbs+u7YyJicn4drCcwCdg325yk=",
"aarch64-linux": "sha256-cTG+gB7CuiFgaYHSFbzXaXUI1C2kiugV3B9rUDRUwlg=", "aarch64-linux": "sha256-z68doReXTrWS7HeiAjc0btIjAsvzeZZ7hXAlHr0c77Q=",
"aarch64-darwin": "sha256-ap4Xv5ZliourkI4mcD0ktbzSI88jb4oVBv5mMK3WdUs=", "aarch64-darwin": "sha256-PILYH1Pi8XBvSkuZ+1sNnUTao5kba+m5Z8iJKx6YXPo=",
"x86_64-darwin": "sha256-NGRJMMnaMNrf3FkcYwr5PcfVm5thhlNLGIeMmAK0MeE=" "x86_64-darwin": "sha256-KpcJzP4m0SUavu/WaSffgzOxrHq8ljdy0GOzs9p16lo="
} }
} }
@@ -0,0 +1,50 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const directory = "C:/OpenCode/PromptInputV2Editing"
const projectID = "proj_prompt_input_v2_editing"
const sessionID = "ses_prompt_input_v2_editing"
test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "prompt-input-v2-editing",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [
{
id: sessionID,
slug: "prompt-input-v2-editing",
projectID,
directory,
title: "Prompt input V2 editing",
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]')
await expectAppVisible(composer)
await input.fill("keep me")
await composer.getByRole("button", { name: "Add images and files" }).click()
await page.getByRole("menuitem", { name: "Commands" }).click()
await page.locator('[data-suggestion-id="model.choose"]').click()
await expect(input).toHaveText("keep me")
})
@@ -56,7 +56,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => {
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
const composer = page.locator('[data-component="prompt-input-v2"]') const composer = page.locator('[data-component="prompt-input-v2"]')
const input = composer.locator('[data-component="prompt-input"]') const input = composer.locator('[data-component="prompt-input"]')
const control = composer.locator('button[title="Choose model variant"]') const control = composer.getByRole("button", { name: "Choose model variant" })
await expectAppVisible(composer) await expectAppVisible(composer)
await idleComposer(page) await idleComposer(page)
@@ -32,6 +32,23 @@ for (const expanded of [false, true]) {
}) })
} }
test("shows and expands a running shell command without shimmering it", async ({ page }) => {
const id = "prt_shell_running_command"
const command = "sleep 10 && echo done"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })],
settings: { shellToolPartsExpanded: false },
})
const tool = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0)
await tool.locator('[data-slot="collapsible-trigger"]').click()
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running")
})
test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => {
const reasoningID = "prt_reasoning_hidden" const reasoningID = "prt_reasoning_hidden"
const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false })
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.18.3", "version": "1.18.4",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -81,7 +81,7 @@
"diff": "catalog:", "diff": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
"luxon": "catalog:", "luxon": "catalog:",
"marked": "catalog:", "marked": "catalog:",
"marked-shiki": "catalog:", "marked-shiki": "catalog:",
@@ -37,6 +37,14 @@ function writeAndWait(term: Terminal, data: string): Promise<void> {
} }
describe("SerializeAddon", () => { describe("SerializeAddon", () => {
test("preserves color scheme reporting mode", async () => {
const { term, addon } = createTerminal()
await writeAndWait(term, "\x1b[?2031h")
expect(addon.serialize().startsWith("\x1b[?2031h")).toBe(true)
expect(addon.serialize({ excludeModes: true }).startsWith("\x1b[?2031h")).toBe(false)
})
describe("ANSI color preservation", () => { describe("ANSI color preservation", () => {
test("should preserve text attributes (bold, italic, underline)", async () => { test("should preserve text attributes (bold, italic, underline)", async () => {
const { term, addon } = createTerminal() const { term, addon } = createTerminal()
+9 -1
View File
@@ -89,6 +89,13 @@ const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined =
return { active, normal, alternate } return { active, normal, alternate }
} }
const getTerminalMode = (value: ITerminalCore, mode: number) => {
if (!isRecord(value)) return false
const terminal = value.wasmTerm
if (!isRecord(terminal) || typeof terminal.getMode !== "function") return false
return terminal.getMode(mode) === true
}
// ============================================================================ // ============================================================================
// Types // Types
// ============================================================================ // ============================================================================
@@ -544,7 +551,8 @@ export class SerializeAddon implements ITerminalAddon {
return "" return ""
} }
let content = options?.range let content = !options?.excludeModes && getTerminalMode(this._terminal, 2031) ? "\u001b[?2031h" : ""
content += options?.range
? this._serializeBufferByRange(normalBuffer, options.range, true) ? this._serializeBufferByRange(normalBuffer, options.range, true)
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback) : this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
+32 -3
View File
@@ -16,6 +16,7 @@ export function TabsInfoPopup() {
const settings = useSettings() const settings = useSettings()
const platform = usePlatform() const platform = usePlatform()
const [drawerOpen, setDrawerOpen] = createSignal(false) const [drawerOpen, setDrawerOpen] = createSignal(false)
const windows = () => platform.platform === "desktop" && platform.os === "windows"
return ( return (
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right"> <Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
@@ -70,12 +71,40 @@ export function TabsInfoPopup() {
</button> </button>
</div> </div>
</Show> </Show>
<DrawerContent> <DrawerContent
<div class="flex h-[52px] w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted p-4"> style={
windows()
? {
inset: "0 0 0 auto",
"max-height": "100vh",
"max-width": "100vw",
"border-radius": "0",
}
: undefined
}
>
<Show when={windows()}>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="neutral"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
class="absolute top-[10px] left-[-36px]"
/>
</Show>
<div
class="flex w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted"
classList={{
"h-[40px] px-4": windows(),
"h-[52px] p-4": !windows(),
}}
>
<p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted"> <p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted">
July 14 July 14
</p> </p>
<Show when={platform.platform !== "desktop" || platform.os !== "windows"}> <Show when={!windows()}>
<DrawerClose <DrawerClose
as={IconButtonV2} as={IconButtonV2}
type="button" type="button"
@@ -36,6 +36,7 @@ import {
export type PromptInputV2ComposerProps = { export type PromptInputV2ComposerProps = {
class?: string class?: string
controller: PromptInputV2ComposerController controller: PromptInputV2ComposerController
borderUnderlay?: boolean
edit?: PromptInputProps["edit"] edit?: PromptInputProps["edit"]
onEditLoaded?: PromptInputProps["onEditLoaded"] onEditLoaded?: PromptInputProps["onEditLoaded"]
} }
@@ -57,7 +58,10 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<PromptInputV2 <PromptInputV2
controller={props.controller} controller={props.controller}
borderUnderlay={props.borderUnderlay}
class={props.class} class={props.class}
attachKeybind={command.keybindParts("file.attach")}
attachShortcut={command.keybind("file.attach")}
modelControl={ modelControl={
<PromptInputV2ModelControl <PromptInputV2ModelControl
loading={props.controller.model.loading} loading={props.controller.model.loading}
@@ -449,12 +453,14 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })),
current: () => props.controls.agents.current, current: () => props.controls.agents.current,
onSelect: props.controls.agents.select, onSelect: props.controls.agents.select,
keybind: () => command.keybindParts("agent.cycle"),
} }
: undefined, : undefined,
variant: { variant: {
options: () => variants().map((value) => ({ id: value, label: value })), options: () => variants().map((value) => ({ id: value, label: value })),
current: () => props.controls.model.selection.variant.current() ?? "default", current: () => props.controls.model.selection.variant.current() ?? "default",
onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value), onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value),
keybind: () => command.keybindParts("model.variant.cycle"),
}, },
submit: { submit: {
stopping, stopping,
+1 -1
View File
@@ -1460,10 +1460,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
t={(key) => language.t(key as Parameters<typeof language.t>[0])} t={(key) => language.t(key as Parameters<typeof language.t>[0])}
/> />
<DockShellForm <DockShellForm
data-dock-border-underlay="legacy"
onSubmit={handleSubmit} onSubmit={handleSubmit}
classList={{ classList={{
"group/prompt-input": true, "group/prompt-input": true,
"focus-within:shadow-xs-border": true,
"border-icon-info-active border-dashed": store.draggingType !== null, "border-icon-info-active border-dashed": store.draggingType !== null,
[props.class ?? ""]: !!props.class, [props.class ?? ""]: !!props.class,
}} }}
@@ -89,13 +89,15 @@ const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID:
} }
export function buildRequestParts(input: BuildRequestPartsInput) { export function buildRequestParts(input: BuildRequestPartsInput) {
const requestParts: PromptRequestPart[] = [ const requestParts: PromptRequestPart[] = input.text.trim()
{ ? [
id: Identifier.ascending("part"), {
type: "text", id: Identifier.ascending("part"),
text: input.text, type: "text",
}, text: input.text,
] },
]
: []
const files = input.prompt.filter(isFileAttachment).map((attachment) => { const files = input.prompt.filter(isFileAttachment).map((attachment) => {
const path = absolute(input.sessionDirectory, attachment.path) const path = absolute(input.sessionDirectory, attachment.path)
@@ -93,24 +93,36 @@ export function PromptWorkspaceSelector(props: {
</MenuV2.Content> </MenuV2.Content>
</MenuV2.Portal> </MenuV2.Portal>
</MenuV2> </MenuV2>
<Show when={props.branch}> <PromptGitStatus branch={props.branch} />
{(branch) => (
<>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
<TooltipV2
placement="top"
value={branch()}
class="min-w-0 max-w-[220px]"
contentClass="max-w-[calc(100vw-32px)] break-all"
>
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{branch()}</span>
</div>
</TooltipV2>
</>
)}
</Show>
</> </>
) )
} }
export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) {
const language = useLanguage()
const label = () => {
if (props.noGit) return language.t("session.new.git.none")
return props.branch
}
return (
<Show when={label()}>
{(value) => (
<>
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
<TooltipV2
placement="top"
value={value()}
class="min-w-0 max-w-[220px]"
contentClass="max-w-[calc(100vw-32px)] break-all"
>
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{value()}</span>
</div>
</TooltipV2>
</>
)}
</Show>
)
}
@@ -133,9 +133,10 @@
} }
[data-slot="settings-v2-row-description"] { [data-slot="settings-v2-row-description"] {
margin-block: -3.5px;
font-size: 13px; font-size: 13px;
font-weight: 440; font-weight: 440;
line-height: 1; line-height: 20px;
color: var(--v2-text-text-muted); color: var(--v2-text-text-muted);
} }
@@ -270,10 +271,10 @@
} }
.settings-v2-provider-description { .settings-v2-provider-description {
margin: 0; margin-block: -3.5px;
font-size: 13px; font-size: 13px;
font-weight: 440; font-weight: 440;
line-height: 1; line-height: 20px;
color: var(--v2-text-text-muted); color: var(--v2-text-text-muted);
} }
+10 -1
View File
@@ -292,7 +292,12 @@ export const Terminal = (props: TerminalProps) => {
const scheduleSize = (cols: number, rows: number) => { const scheduleSize = (cols: number, rows: number) => {
if (disposed) return if (disposed) return
if (lastSize?.cols === cols && lastSize?.rows === rows) return if (lastSize?.cols === cols && lastSize?.rows === rows) {
pendingSize = undefined
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
sizeTimer = undefined
return
}
pendingSize = { cols, rows } pendingSize = { cols, rows }
@@ -317,8 +322,10 @@ export const Terminal = (props: TerminalProps) => {
createEffect(() => { createEffect(() => {
const colors = terminalColors() const colors = terminalColors()
const mode = theme.mode() === "dark" ? "dark" : "light"
if (!term) return if (!term) return
setOptionIfSupported(term, "theme", colors) setOptionIfSupported(term, "theme", colors)
setOptionIfSupported(term, "colorScheme", mode)
}) })
createEffect(() => { createEffect(() => {
@@ -396,6 +403,7 @@ export const Terminal = (props: TerminalProps) => {
} }
_ghostty = g _ghostty = g
term = t term = t
setOptionIfSupported(t, "colorScheme", theme.mode() === "dark" ? "dark" : "light")
output = terminalWriter((data, done) => output = terminalWriter((data, done) =>
t.write(data, () => { t.write(data, () => {
done?.() done?.()
@@ -595,6 +603,7 @@ export const Terminal = (props: TerminalProps) => {
tries = 0 tries = 0
local.onConnect?.() local.onConnect?.()
scheduleSize(t.cols, t.rows) scheduleSize(t.cols, t.rows)
if (t.getMode(2031)) t.write("\x1b[?996n")
} }
const handleMessage = (event: MessageEvent) => { const handleMessage = (event: MessageEvent) => {
+21 -6
View File
@@ -67,7 +67,7 @@ export function useTitlebarRightMount() {
return mount return mount
} }
export function Titlebar(props: { update?: TitlebarUpdate }) { export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
const layout = useLayout() const layout = useLayout()
const platform = usePlatform() const platform = usePlatform()
const command = useCommand() const command = useCommand()
@@ -462,7 +462,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
"md:pl-4": !mac(), "md:pl-4": !mac(),
}} }}
> >
<ChannelIndicator /> <ChannelIndicator debugTools={props.debugTools} />
<Show when={windows() || linux()}> <Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} variant="v2" /> <WindowsAppMenu command={command} platform={platform} variant="v2" />
</Show> </Show>
@@ -660,9 +660,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
</div> </div>
</Show> </Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" /> <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
<ChannelIndicator />
</div> </div>
</div> </div>
<ChannelIndicator debugTools={props.debugTools} />
</div> </div>
</div> </div>
@@ -747,12 +747,27 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
) )
} }
function ChannelIndicator() { function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () => void } }) {
const channel = import.meta.env.VITE_OPENCODE_CHANNEL
if (channel === "dev" && props.debugTools) {
return (
<button
type="button"
class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono cursor-pointer"
onClick={props.debugTools.toggle}
aria-label="Toggle debug tools"
aria-pressed={props.debugTools.visible}
>
DEV
</button>
)
}
return ( return (
<> <>
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && ( {["beta", "dev"].includes(channel) && (
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono"> <div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()} {channel.toUpperCase()}
</div> </div>
)} )}
</> </>
+8 -1
View File
@@ -176,7 +176,14 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
onCleanup(() => states.forEach((value) => value.dispose())) onCleanup(() => states.forEach((value) => value.dispose()))
const selected = () => ensure(activeServer()) const selected = () => {
const list = global.servers.list()
const key = activeServer()
if (list.some((conn) => ServerConnection.key(conn) === key)) return ensure(key)
const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0]
if (!conn) throw new Error("Notification server not found")
return ensure(ServerConnection.key(conn))
}
return { return {
ready: () => selected().ready(), ready: () => selected().ready(),
@@ -200,6 +200,49 @@ describe("server session", () => {
expect(store.history.more("child")).toBe(true) expect(store.history.more("child")).toBe(true)
}) })
test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const assistant = assistantMessage("message-2", "message-missing")
const client = rootMessageClient([response([{ info: assistant, parts: [] }], "older")], [missing.promise])
const store = createServerSession(client)
const loading = store.sync("child")
await client.rootRequested(1)
missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } }))
await loading
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }])
expect(store.data.message.child).toEqual([assistant])
expect(store.history.more("child")).toBe(true)
})
test("drops a cached parent when a forced refresh confirms it was deleted", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const parent = userMessage("message-1")
const part = textPart(parent.id)
const assistant = assistantMessage("message-2", parent.id)
const client = rootMessageClient(
[
response([
{ info: parent, parts: [part] },
{ info: assistant, parts: [] },
]),
response([{ info: assistant, parts: [] }], "older"),
],
[missing.promise],
)
const store = createServerSession(client)
await store.sync("child")
const loading = store.sync("child", { force: true })
await client.rootRequested(1)
missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } }))
await loading
expect(store.data.message.child).toEqual([assistant])
expect(store.data.part[parent.id]).toBeUndefined()
})
test("does not let an optimistic user suppress initial root backfill", async () => { test("does not let an optimistic user suppress initial root backfill", async () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const part = textPart(user.id) const part = textPart(user.id)
+12 -1
View File
@@ -110,6 +110,7 @@ function reconcileFetched<T extends { id: string }>(
options: { options: {
touched?: ReadonlySet<string> touched?: ReadonlySet<string>
retained?: ReadonlySet<string> retained?: ReadonlySet<string>
removed?: ReadonlySet<string>
preserveUnfetched?: boolean | ((item: T) => boolean) preserveUnfetched?: boolean | ((item: T) => boolean)
} = {}, } = {},
) { ) {
@@ -132,6 +133,7 @@ function reconcileFetched<T extends { id: string }>(
if (item) result.set(id, item) if (item) result.set(id, item)
if (!item) result.delete(id) if (!item) result.delete(id)
} }
for (const id of options.removed ?? emptyIDs) result.delete(id)
return [...result.values()].sort((a, b) => cmp(a.id, b.id)) return [...result.values()].sort((a, b) => cmp(a.id, b.id))
} }
@@ -577,6 +579,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], { const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], {
touched: touchedMessages, touched: touchedMessages,
retained: load?.retainedMessages, retained: load?.retainedMessages,
removed: load?.removedMessages,
preserveUnfetched, preserveUnfetched,
}) })
batch(() => { batch(() => {
@@ -645,7 +648,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
if (generations.get(sessionID) !== active) break if (generations.get(sessionID) !== active) break
const parent = await fetchMessage(sessionID, parentID, () => const parent = await fetchMessage(sessionID, parentID, () =>
resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)), resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)),
) ).catch((error) => {
const cause = error instanceof Error && typeof error.cause === "object" ? error.cause : undefined
if (cause && "status" in cause && cause.status === 404) {
load.removedMessages.add(parentID)
return
}
throw error
})
if (!parent) continue
if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`) if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`)
parents.push(parent) parents.push(parent)
} }
+1 -1
View File
@@ -75,7 +75,6 @@ export const dict = {
"command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي", "command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي",
"command.session.undo": "تراجع", "command.session.undo": "تراجع",
"command.session.undo.description": "تراجع عن الرسالة الأخيرة", "command.session.undo.description": "تراجع عن الرسالة الأخيرة",
"command.session.undo.gitRequired": "لم يتم اكتشاف مستودع Git",
"command.session.redo": "إعادة", "command.session.redo": "إعادة",
"command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها", "command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها",
"command.session.compact": "ضغط الجلسة", "command.session.compact": "ضغط الجلسة",
@@ -614,6 +613,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "محلي", "session.new.workspace.triggerLocal": "محلي",
"session.new.workspace.local": "المستودع المحلي", "session.new.workspace.local": "المستودع المحلي",
"session.new.workspace.existing": "مساحة عمل…", "session.new.workspace.existing": "مساحة عمل…",
"session.new.git.none": "لا يوجد Git",
"session.new.lastModified": "آخر تعديل", "session.new.lastModified": "آخر تعديل",
"session.header.search.placeholder": "بحث {{project}}", "session.header.search.placeholder": "بحث {{project}}",
"session.header.searchFiles": "بحث عن الملفات", "session.header.searchFiles": "بحث عن الملفات",
+1 -1
View File
@@ -75,7 +75,6 @@ export const dict = {
"command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral", "command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral",
"command.session.undo": "Desfazer", "command.session.undo": "Desfazer",
"command.session.undo.description": "Desfazer a última mensagem", "command.session.undo.description": "Desfazer a última mensagem",
"command.session.undo.gitRequired": "Nenhum repositório Git detectado",
"command.session.redo": "Refazer", "command.session.redo": "Refazer",
"command.session.redo.description": "Refazer a última mensagem desfeita", "command.session.redo.description": "Refazer a última mensagem desfeita",
"command.session.compact": "Compactar sessão", "command.session.compact": "Compactar sessão",
@@ -620,6 +619,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Local", "session.new.workspace.triggerLocal": "Local",
"session.new.workspace.local": "Repositório local", "session.new.workspace.local": "Repositório local",
"session.new.workspace.existing": "Espaço de trabalho…", "session.new.workspace.existing": "Espaço de trabalho…",
"session.new.git.none": "Sem Git",
"session.new.lastModified": "Última modificação", "session.new.lastModified": "Última modificação",
"session.header.search.placeholder": "Buscar {{project}}", "session.header.search.placeholder": "Buscar {{project}}",
"session.header.searchFiles": "Buscar arquivos", "session.header.searchFiles": "Buscar arquivos",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci", "command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci",
"command.session.undo": "Poništi", "command.session.undo": "Poništi",
"command.session.undo.description": "Poništi posljednju poruku", "command.session.undo.description": "Poništi posljednju poruku",
"command.session.undo.gitRequired": "Git repozitorij nije otkriven",
"command.session.redo": "Vrati", "command.session.redo": "Vrati",
"command.session.redo.description": "Vrati posljednju poništenu poruku", "command.session.redo.description": "Vrati posljednju poništenu poruku",
"command.session.compact": "Sažmi sesiju", "command.session.compact": "Sažmi sesiju",
@@ -676,6 +675,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Lokalno", "session.new.workspace.triggerLocal": "Lokalno",
"session.new.workspace.local": "Lokalni repozitorij", "session.new.workspace.local": "Lokalni repozitorij",
"session.new.workspace.existing": "Radni prostor…", "session.new.workspace.existing": "Radni prostor…",
"session.new.git.none": "Nema Gita",
"session.new.lastModified": "Posljednja izmjena", "session.new.lastModified": "Posljednja izmjena",
"session.header.search.placeholder": "Pretraži {{project}}", "session.header.search.placeholder": "Pretraži {{project}}",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken", "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken",
"command.session.undo": "Fortryd", "command.session.undo": "Fortryd",
"command.session.undo.description": "Fortryd den sidste besked", "command.session.undo.description": "Fortryd den sidste besked",
"command.session.undo.gitRequired": "Intet Git-repository fundet",
"command.session.redo": "Omgør", "command.session.redo": "Omgør",
"command.session.redo.description": "Omgør den sidste fortrudte besked", "command.session.redo.description": "Omgør den sidste fortrudte besked",
"command.session.compact": "Komprimér session", "command.session.compact": "Komprimér session",
@@ -671,6 +670,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Lokal", "session.new.workspace.triggerLocal": "Lokal",
"session.new.workspace.local": "Lokalt repository", "session.new.workspace.local": "Lokalt repository",
"session.new.workspace.existing": "Arbejdsområde…", "session.new.workspace.existing": "Arbejdsområde…",
"session.new.git.none": "Ingen Git",
"session.new.lastModified": "Sidst ændret", "session.new.lastModified": "Sidst ændret",
"session.header.search.placeholder": "Søg {{project}}", "session.header.search.placeholder": "Søg {{project}}",
+1 -1
View File
@@ -79,7 +79,6 @@ export const dict = {
"command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren", "command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren",
"command.session.undo": "Rückgängig", "command.session.undo": "Rückgängig",
"command.session.undo.description": "Letzte Nachricht rückgängig machen", "command.session.undo.description": "Letzte Nachricht rückgängig machen",
"command.session.undo.gitRequired": "Kein Git-Repository erkannt",
"command.session.redo": "Wiederherstellen", "command.session.redo": "Wiederherstellen",
"command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen", "command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen",
"command.session.compact": "Sitzung komprimieren", "command.session.compact": "Sitzung komprimieren",
@@ -629,6 +628,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Lokal", "session.new.workspace.triggerLocal": "Lokal",
"session.new.workspace.local": "Lokales Repository", "session.new.workspace.local": "Lokales Repository",
"session.new.workspace.existing": "Arbeitsbereich…", "session.new.workspace.existing": "Arbeitsbereich…",
"session.new.git.none": "Kein Git",
"session.new.lastModified": "Zuletzt geändert", "session.new.lastModified": "Zuletzt geändert",
"session.header.search.placeholder": "{{project}} durchsuchen", "session.header.search.placeholder": "{{project}} durchsuchen",
"session.header.searchFiles": "Dateien suchen", "session.header.searchFiles": "Dateien suchen",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
"command.session.undo": "Undo", "command.session.undo": "Undo",
"command.session.undo.description": "Undo the last message", "command.session.undo.description": "Undo the last message",
"command.session.undo.gitRequired": "No Git repository detected",
"command.session.redo": "Redo", "command.session.redo": "Redo",
"command.session.redo.description": "Redo the last undone message", "command.session.redo.description": "Redo the last undone message",
"command.session.compact": "Compact session", "command.session.compact": "Compact session",
@@ -701,6 +700,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Local", "session.new.workspace.triggerLocal": "Local",
"session.new.workspace.local": "Local repository", "session.new.workspace.local": "Local repository",
"session.new.workspace.existing": "Workspace…", "session.new.workspace.existing": "Workspace…",
"session.new.git.none": "No Git",
"session.new.lastModified": "Last modified", "session.new.lastModified": "Last modified",
"session.header.search.placeholder": "Search {{project}}", "session.header.search.placeholder": "Search {{project}}",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral", "command.workspace.toggle.description": "Habilitar o deshabilitar múltiples espacios de trabajo en la barra lateral",
"command.session.undo": "Deshacer", "command.session.undo": "Deshacer",
"command.session.undo.description": "Deshacer el último mensaje", "command.session.undo.description": "Deshacer el último mensaje",
"command.session.undo.gitRequired": "No se detectó ningún repositorio Git",
"command.session.redo": "Rehacer", "command.session.redo": "Rehacer",
"command.session.redo.description": "Rehacer el último mensaje deshecho", "command.session.redo.description": "Rehacer el último mensaje deshecho",
"command.session.compact": "Compactar sesión", "command.session.compact": "Compactar sesión",
@@ -677,6 +676,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Local", "session.new.workspace.triggerLocal": "Local",
"session.new.workspace.local": "Repositorio local", "session.new.workspace.local": "Repositorio local",
"session.new.workspace.existing": "Espacio de trabajo…", "session.new.workspace.existing": "Espacio de trabajo…",
"session.new.git.none": "Sin Git",
"session.new.lastModified": "Última modificación", "session.new.lastModified": "Última modificación",
"session.header.search.placeholder": "Buscar {{project}}", "session.header.search.placeholder": "Buscar {{project}}",
+1 -1
View File
@@ -75,7 +75,6 @@ export const dict = {
"command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale", "command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale",
"command.session.undo": "Annuler", "command.session.undo": "Annuler",
"command.session.undo.description": "Annuler le dernier message", "command.session.undo.description": "Annuler le dernier message",
"command.session.undo.gitRequired": "Aucun dépôt Git détecté",
"command.session.redo": "Rétablir", "command.session.redo": "Rétablir",
"command.session.redo.description": "Rétablir le dernier message annulé", "command.session.redo.description": "Rétablir le dernier message annulé",
"command.session.compact": "Compacter la session", "command.session.compact": "Compacter la session",
@@ -625,6 +624,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Local", "session.new.workspace.triggerLocal": "Local",
"session.new.workspace.local": "Dépôt local", "session.new.workspace.local": "Dépôt local",
"session.new.workspace.existing": "Espace de travail…", "session.new.workspace.existing": "Espace de travail…",
"session.new.git.none": "Pas de Git",
"session.new.lastModified": "Dernière modification", "session.new.lastModified": "Dernière modification",
"session.header.search.placeholder": "Rechercher {{project}}", "session.header.search.placeholder": "Rechercher {{project}}",
"session.header.searchFiles": "Rechercher des fichiers", "session.header.searchFiles": "Rechercher des fichiers",
+1 -1
View File
@@ -75,7 +75,6 @@ export const dict = {
"command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化", "command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化",
"command.session.undo": "元に戻す", "command.session.undo": "元に戻す",
"command.session.undo.description": "最後のメッセージを元に戻す", "command.session.undo.description": "最後のメッセージを元に戻す",
"command.session.undo.gitRequired": "Git リポジトリが検出されませんでした",
"command.session.redo": "やり直す", "command.session.redo": "やり直す",
"command.session.redo.description": "元に戻したメッセージをやり直す", "command.session.redo.description": "元に戻したメッセージをやり直す",
"command.session.compact": "セッションを圧縮", "command.session.compact": "セッションを圧縮",
@@ -616,6 +615,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "ローカル", "session.new.workspace.triggerLocal": "ローカル",
"session.new.workspace.local": "ローカルリポジトリ", "session.new.workspace.local": "ローカルリポジトリ",
"session.new.workspace.existing": "ワークスペース…", "session.new.workspace.existing": "ワークスペース…",
"session.new.git.none": "Git なし",
"session.new.lastModified": "最終更新", "session.new.lastModified": "最終更新",
"session.header.search.placeholder": "{{project}}を検索", "session.header.search.placeholder": "{{project}}を検索",
"session.header.searchFiles": "ファイルを検索", "session.header.searchFiles": "ファイルを検索",
+1 -1
View File
@@ -71,7 +71,6 @@ export const dict = {
"command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화", "command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화",
"command.session.undo": "실행 취소", "command.session.undo": "실행 취소",
"command.session.undo.description": "마지막 메시지 실행 취소", "command.session.undo.description": "마지막 메시지 실행 취소",
"command.session.undo.gitRequired": "Git 저장소가 감지되지 않았습니다",
"command.session.redo": "다시 실행", "command.session.redo": "다시 실행",
"command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행",
"command.session.compact": "세션 압축", "command.session.compact": "세션 압축",
@@ -977,6 +976,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "로컬", "session.new.workspace.triggerLocal": "로컬",
"session.new.workspace.local": "로컬 저장소", "session.new.workspace.local": "로컬 저장소",
"session.new.workspace.existing": "작업 공간…", "session.new.workspace.existing": "작업 공간…",
"session.new.git.none": "Git 없음",
"sidebar.empty.title": "열린 프로젝트 없음", "sidebar.empty.title": "열린 프로젝트 없음",
"sidebar.empty.description": "프로젝트를 열어 시작하세요", "sidebar.empty.description": "프로젝트를 열어 시작하세요",
+1 -1
View File
@@ -80,7 +80,6 @@ export const dict = {
"command.workspace.toggle.description": "Aktiver eller deaktiver flere arbeidsområder i sidefeltet", "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbeidsområder i sidefeltet",
"command.session.undo": "Angre", "command.session.undo": "Angre",
"command.session.undo.description": "Angre siste melding", "command.session.undo.description": "Angre siste melding",
"command.session.undo.gitRequired": "Fant ikke noe Git-repositorium",
"command.session.redo": "Gjør om", "command.session.redo": "Gjør om",
"command.session.redo.description": "Gjør om siste angrede melding", "command.session.redo.description": "Gjør om siste angrede melding",
"command.session.compact": "Komprimer sesjon", "command.session.compact": "Komprimer sesjon",
@@ -1071,6 +1070,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Lokalt", "session.new.workspace.triggerLocal": "Lokalt",
"session.new.workspace.local": "Lokalt depot", "session.new.workspace.local": "Lokalt depot",
"session.new.workspace.existing": "Arbeidsområde…", "session.new.workspace.existing": "Arbeidsområde…",
"session.new.git.none": "Ingen Git",
"sidebar.empty.title": "Ingen åpne prosjekter", "sidebar.empty.title": "Ingen åpne prosjekter",
"sidebar.empty.description": "Åpne et prosjekt for å komme i gang", "sidebar.empty.description": "Åpne et prosjekt for å komme i gang",
+1 -1
View File
@@ -75,7 +75,6 @@ export const dict = {
"command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym", "command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym",
"command.session.undo": "Cofnij", "command.session.undo": "Cofnij",
"command.session.undo.description": "Cofnij ostatnią wiadomość", "command.session.undo.description": "Cofnij ostatnią wiadomość",
"command.session.undo.gitRequired": "Nie wykryto repozytorium Git",
"command.session.redo": "Ponów", "command.session.redo": "Ponów",
"command.session.redo.description": "Ponów ostatnią cofniętą wiadomość", "command.session.redo.description": "Ponów ostatnią cofniętą wiadomość",
"command.session.compact": "Kompaktuj sesję", "command.session.compact": "Kompaktuj sesję",
@@ -620,6 +619,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Lokalnie", "session.new.workspace.triggerLocal": "Lokalnie",
"session.new.workspace.local": "Lokalne repozytorium", "session.new.workspace.local": "Lokalne repozytorium",
"session.new.workspace.existing": "Przestrzeń robocza…", "session.new.workspace.existing": "Przestrzeń robocza…",
"session.new.git.none": "Brak Git",
"session.new.lastModified": "Ostatnio zmodyfikowano", "session.new.lastModified": "Ostatnio zmodyfikowano",
"session.header.search.placeholder": "Szukaj {{project}}", "session.header.search.placeholder": "Szukaj {{project}}",
"session.header.searchFiles": "Szukaj plików", "session.header.searchFiles": "Szukaj plików",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели", "command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели",
"command.session.undo": "Отменить", "command.session.undo": "Отменить",
"command.session.undo.description": "Отменить последнее сообщение", "command.session.undo.description": "Отменить последнее сообщение",
"command.session.undo.gitRequired": "Репозиторий Git не обнаружен",
"command.session.redo": "Повторить", "command.session.redo": "Повторить",
"command.session.redo.description": "Повторить отменённое сообщение", "command.session.redo.description": "Повторить отменённое сообщение",
"command.session.compact": "Сжать сессию", "command.session.compact": "Сжать сессию",
@@ -674,6 +673,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Локально", "session.new.workspace.triggerLocal": "Локально",
"session.new.workspace.local": "Локальный репозиторий", "session.new.workspace.local": "Локальный репозиторий",
"session.new.workspace.existing": "Рабочее пространство…", "session.new.workspace.existing": "Рабочее пространство…",
"session.new.git.none": "Нет Git",
"session.new.lastModified": "Последнее изменение", "session.new.lastModified": "Последнее изменение",
"session.header.search.placeholder": "Поиск {{project}}", "session.header.search.placeholder": "Поиск {{project}}",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง", "command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง",
"command.session.undo": "ยกเลิก", "command.session.undo": "ยกเลิก",
"command.session.undo.description": "ยกเลิกข้อความล่าสุด", "command.session.undo.description": "ยกเลิกข้อความล่าสุด",
"command.session.undo.gitRequired": "ไม่พบที่เก็บ Git",
"command.session.redo": "ทำซ้ำ", "command.session.redo": "ทำซ้ำ",
"command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด", "command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด",
"command.session.compact": "บีบอัดเซสชัน", "command.session.compact": "บีบอัดเซสชัน",
@@ -670,6 +669,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "ในเครื่อง", "session.new.workspace.triggerLocal": "ในเครื่อง",
"session.new.workspace.local": "รีโพซิทอรีในเครื่อง", "session.new.workspace.local": "รีโพซิทอรีในเครื่อง",
"session.new.workspace.existing": "พื้นที่ทำงาน…", "session.new.workspace.existing": "พื้นที่ทำงาน…",
"session.new.git.none": "ไม่มี Git",
"session.new.lastModified": "แก้ไขล่าสุด", "session.new.lastModified": "แก้ไขล่าสุด",
"session.header.search.placeholder": "ค้นหา {{project}}", "session.header.search.placeholder": "ค้นหา {{project}}",
+1 -1
View File
@@ -85,7 +85,6 @@ export const dict = {
"command.workspace.toggle.description": "Kenar çubuğunda birden fazla çalışma alanını göster veya gizle", "command.workspace.toggle.description": "Kenar çubuğunda birden fazla çalışma alanını göster veya gizle",
"command.session.undo": "Geri al", "command.session.undo": "Geri al",
"command.session.undo.description": "Son mesajı geri al", "command.session.undo.description": "Son mesajı geri al",
"command.session.undo.gitRequired": "Git deposu algılanmadı",
"command.session.redo": "Yinele", "command.session.redo": "Yinele",
"command.session.redo.description": "Son geri alınan mesajı yinele", "command.session.redo.description": "Son geri alınan mesajı yinele",
"command.session.compact": "Oturumu sıkıştır", "command.session.compact": "Oturumu sıkıştır",
@@ -680,6 +679,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Yerel", "session.new.workspace.triggerLocal": "Yerel",
"session.new.workspace.local": "Yerel depo", "session.new.workspace.local": "Yerel depo",
"session.new.workspace.existing": "Çalışma alanı…", "session.new.workspace.existing": "Çalışma alanı…",
"session.new.git.none": "Git yok",
"session.new.lastModified": "Son değişiklik", "session.new.lastModified": "Son değişiklik",
"session.header.search.placeholder": "{{project}} ara", "session.header.search.placeholder": "{{project}} ara",
+1 -1
View File
@@ -81,7 +81,6 @@ export const dict = {
"command.workspace.toggle.description": "Увімкнути або вимкнути декілька робочих областей на бічній панелі", "command.workspace.toggle.description": "Увімкнути або вимкнути декілька робочих областей на бічній панелі",
"command.session.undo": "Скасувати", "command.session.undo": "Скасувати",
"command.session.undo.description": "Скасувати останнє повідомлення", "command.session.undo.description": "Скасувати останнє повідомлення",
"command.session.undo.gitRequired": "Репозиторій Git не виявлено",
"command.session.redo": "Повторити", "command.session.redo": "Повторити",
"command.session.redo.description": "Повторити останнє скасоване повідомлення", "command.session.redo.description": "Повторити останнє скасоване повідомлення",
"command.session.compact": "Стиснути сесію", "command.session.compact": "Стиснути сесію",
@@ -704,6 +703,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "Локально", "session.new.workspace.triggerLocal": "Локально",
"session.new.workspace.local": "Локальний репозиторій", "session.new.workspace.local": "Локальний репозиторій",
"session.new.workspace.existing": "Робоча область…", "session.new.workspace.existing": "Робоча область…",
"session.new.git.none": "Немає Git",
"session.new.lastModified": "Востаннє змінено", "session.new.lastModified": "Востаннє змінено",
"session.header.search.placeholder": "Пошук {{project}}", "session.header.search.placeholder": "Пошук {{project}}",
+1 -1
View File
@@ -108,7 +108,6 @@ export const dict = {
"command.session.undo": "撤销", "command.session.undo": "撤销",
"command.session.undo.description": "撤销上一条消息", "command.session.undo.description": "撤销上一条消息",
"command.session.undo.gitRequired": "未检测到 Git 仓库",
"command.session.redo": "重做", "command.session.redo": "重做",
"command.session.redo.description": "重做上一条撤销的消息", "command.session.redo.description": "重做上一条撤销的消息",
"command.session.compact": "精简会话", "command.session.compact": "精简会话",
@@ -667,6 +666,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "本地", "session.new.workspace.triggerLocal": "本地",
"session.new.workspace.local": "本地仓库", "session.new.workspace.local": "本地仓库",
"session.new.workspace.existing": "工作区…", "session.new.workspace.existing": "工作区…",
"session.new.git.none": "无 Git",
"session.new.lastModified": "最后修改", "session.new.lastModified": "最后修改",
"session.header.search.placeholder": "搜索 {{project}}", "session.header.search.placeholder": "搜索 {{project}}",
"session.header.searchFiles": "搜索文件", "session.header.searchFiles": "搜索文件",
+1 -1
View File
@@ -85,7 +85,6 @@ export const dict = {
"command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區", "command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區",
"command.session.undo": "復原", "command.session.undo": "復原",
"command.session.undo.description": "復原上一則訊息", "command.session.undo.description": "復原上一則訊息",
"command.session.undo.gitRequired": "未偵測到 Git 儲存庫",
"command.session.redo": "重做", "command.session.redo": "重做",
"command.session.redo.description": "重做上一則復原的訊息", "command.session.redo.description": "重做上一則復原的訊息",
"command.session.compact": "精簡工作階段", "command.session.compact": "精簡工作階段",
@@ -664,6 +663,7 @@ export const dict = {
"session.new.workspace.triggerLocal": "本機", "session.new.workspace.triggerLocal": "本機",
"session.new.workspace.local": "本機儲存庫", "session.new.workspace.local": "本機儲存庫",
"session.new.workspace.existing": "工作區…", "session.new.workspace.existing": "工作區…",
"session.new.git.none": "無 Git",
"session.new.lastModified": "最後修改", "session.new.lastModified": "最後修改",
"session.header.search.placeholder": "搜尋 {{project}}", "session.header.search.placeholder": "搜尋 {{project}}",
+128 -25
View File
@@ -17,6 +17,11 @@ import {
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers"
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
import { useQuery } from "@tanstack/solid-query" import { useQuery } from "@tanstack/solid-query"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo" import { Logo } from "@opencode-ai/ui/logo"
@@ -1037,7 +1042,7 @@ function HomeServerRow(props: {
) )
} }
function HomeProjectList(props: { type HomeProjectListProps = {
server: ServerConnection.Any server: ServerConnection.Any
projects: LocalProject[] projects: LocalProject[]
selected: HomeProjectSelection selected: HomeProjectSelection
@@ -1048,29 +1053,82 @@ function HomeProjectList(props: {
clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}) { }
function HomeProjectList(props: HomeProjectListProps) {
const global = useGlobal()
let listRef!: HTMLDivElement
const projects = () => global.ensureServerCtx(props.server).projects
return ( return (
<div class="flex min-w-0 flex-col gap-1"> <DragDropProvider
<For each={props.projects}> sensors={(defaults) => [
{(project) => ( ...defaults.filter((sensor) => sensor !== PointerSensor),
<HomeProjectRow PointerSensor.configure({
project={project} activationConstraints: (event) =>
server={props.server} event.pointerType === "touch"
selected={ ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })]
props.selected.server === ServerConnection.key(props.server) && : [new PointerActivationConstraints.Distance({ value: 4 })],
props.selected.directory === project.worktree preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"),
} }),
unseenCount={props.unseenCount(props.server, project)} ]}
selectProject={props.selectProject} modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]}
openNewSession={props.openNewSession} plugins={(defaults) => [
editProject={props.editProject} ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback),
closeProject={props.closeProject} AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }),
clearNotifications={props.clearNotifications} Feedback.configure({ dropAnimation: null }),
language={props.language} ]}
/> onDragEnd={(event) => {
)} const source = event.operation.source
</For> if (event.canceled || !isSortable(source)) return
</div> if (source.initialIndex !== source.index) projects().move(source.id.toString(), source.index)
if (props.selected.server !== ServerConnection.key(props.server))
props.selectProject(props.server, source.id.toString())
}}
>
<div class="flex min-w-0 flex-col gap-1" ref={listRef}>
{/* Keyed on worktree strings: the enriched project objects are
recreated on every store or sync update, so iterating them directly
remounts all rows killing any in-flight drag activation (the
row's sortable unregisters on unmount) and discarding animations.
String keys keep row elements alive and move them on reorder. */}
<For each={props.projects.map((project) => project.worktree)}>
{(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index} />}
</For>
</div>
</DragDropProvider>
)
}
function HomeProjectSlot(
props: HomeProjectListProps & {
worktree: string
index: () => number
},
) {
const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree))
return (
<Show when={project()}>
{(item) => (
<HomeProjectRow
project={item()}
server={props.server}
index={props.index}
serverSelected={props.selected.server === ServerConnection.key(props.server)}
selected={
props.selected.server === ServerConnection.key(props.server) && props.selected.directory === props.worktree
}
unseenCount={props.unseenCount(props.server, item())}
selectProject={props.selectProject}
openNewSession={props.openNewSession}
editProject={props.editProject}
closeProject={props.closeProject}
clearNotifications={props.clearNotifications}
language={props.language}
/>
)}
</Show>
) )
} }
@@ -1150,6 +1208,8 @@ function HomeRecentlyClosedRow(props: {
function HomeProjectRow(props: { function HomeProjectRow(props: {
project: LocalProject project: LocalProject
server: ServerConnection.Any server: ServerConnection.Any
index: () => number
serverSelected: boolean
selected: boolean selected: boolean
unseenCount: number unseenCount: number
selectProject: (server: ServerConnection.Any, directory: string) => void selectProject: (server: ServerConnection.Any, directory: string) => void
@@ -1163,6 +1223,15 @@ function HomeProjectRow(props: {
const platform = usePlatform() const platform = usePlatform()
const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false
const [state, setState] = createStore({ menuOpen: false }) const [state, setState] = createStore({ menuOpen: false })
const sortable = useSortable({
get id() {
return props.project.worktree
},
get index() {
return props.index()
},
})
let pointerDownSelected: boolean | undefined
const canRevealInFileManager = () => const canRevealInFileManager = () =>
platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(props.server) platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(props.server)
const fileManagerActionLabel = () => const fileManagerActionLabel = () =>
@@ -1179,15 +1248,49 @@ function HomeProjectRow(props: {
) )
} }
return ( return (
<div class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"> <div
ref={sortable.ref}
class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"
classList={{ "z-10": sortable.isDragSource() }}
>
<button <button
type="button" type="button"
data-component="home-project-row" data-component="home-project-row"
class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`} class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
classList={{
"bg-v2-background-bg-layer-01 text-v2-text-text-base [box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]":
sortable.isDragSource(),
}}
data-selected={props.selected ? "" : undefined} data-selected={props.selected ? "" : undefined}
aria-current={props.selected ? "page" : undefined} aria-current={props.selected ? "page" : undefined}
disabled={serverUnreachable()} disabled={serverUnreachable()}
onClick={() => props.selectProject(props.server, props.project.worktree)} onPointerDown={(event) => {
// Same-server mouse selection happens on pointerdown (like tabs),
// but only ever selects; selectProject toggles, and deselecting here
// would fire on every drag before the threshold is met. Cross-server
// selection waits for click so reordering a remote server's projects
// does not focus that server and load its session index. Touch is
// excluded so flick-scrolling the list cannot select rows.
pointerDownSelected = undefined
if (event.button !== 0 || event.pointerType === "touch") return
if (!props.serverSelected) return
pointerDownSelected = props.selected
if (!props.selected) props.selectProject(props.server, props.project.worktree)
}}
onClick={(event) => {
// The drag sensor calls preventDefault on post-drag clicks; never
// toggle selection as part of a reorder.
if (event.defaultPrevented) return
// Keyboard activation and touch taps keep the original toggle.
if (event.detail === 0 || pointerDownSelected === undefined) {
props.selectProject(props.server, props.project.worktree)
return
}
// Mouse: pointerdown already selected unselected rows; a plain click
// on an already-selected row toggles it off.
if (pointerDownSelected) props.selectProject(props.server, props.project.worktree)
pointerDownSelected = undefined
}}
> >
<HomeProjectAvatar project={props.project} /> <HomeProjectAvatar project={props.project} />
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span> <span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
+11 -2
View File
@@ -1,4 +1,5 @@
import { createEffect, Suspense, type ParentProps } from "solid-js" import { createEffect, Suspense, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { DebugBar } from "@/components/debug-bar" import { DebugBar } from "@/components/debug-bar"
import { TabsInfoPopup } from "@/components/help-button" import { TabsInfoPopup } from "@/components/help-button"
@@ -11,6 +12,7 @@ export default function NewLayout(props: ParentProps) {
const platform = usePlatform() const platform = usePlatform()
const navigate = useNavigate() const navigate = useNavigate()
setNavigate(navigate) setNavigate(navigate)
const [state, setState] = createStore({ debugTools: true })
createEffect(() => setV2Toast(true)) createEffect(() => setV2Toast(true))
@@ -32,11 +34,18 @@ export default function NewLayout(props: ParentProps) {
"padding-bottom": "env(safe-area-inset-bottom, 0px)", "padding-bottom": "env(safe-area-inset-bottom, 0px)",
}} }}
> >
<Titlebar update={update} /> <Titlebar
update={update}
debugTools={
import.meta.env.DEV
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
: undefined
}
/>
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict"> <main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
</main> </main>
{import.meta.env.DEV && <DebugBar inline />} {import.meta.env.DEV && state.debugTools && <DebugBar inline />}
<TabsInfoPopup /> <TabsInfoPopup />
<ToastRegion v2 /> <ToastRegion v2 />
</div> </div>
+10 -2
View File
@@ -156,6 +156,7 @@ export default function LegacyLayout(props: ParentProps) {
sizing: false, sizing: false,
peek: undefined as string | undefined, peek: undefined as string | undefined,
peeked: false, peeked: false,
debugTools: true,
}) })
const updateVersion = () => { const updateVersion = () => {
@@ -2248,7 +2249,14 @@ export default function LegacyLayout(props: ParentProps) {
return ( return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""} {autoselecting() ?? ""}
<Titlebar update={titlebarUpdate} /> <Titlebar
update={titlebarUpdate}
debugTools={
import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1"
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
: undefined
}
/>
<Show when={updateVersion() !== undefined}> <Show when={updateVersion() !== undefined}>
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} /> <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
</Show> </Show>
@@ -2393,7 +2401,7 @@ export default function LegacyLayout(props: ParentProps) {
</div> </div>
</div> </div>
</div> </div>
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />} {import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && state.debugTools && <DebugBar />}
</div> </div>
<TabsInfoPopup /> <TabsInfoPopup />
<ToastRegion v2={false} /> <ToastRegion v2={false} />
@@ -13,7 +13,6 @@ export function useSessionTabAvatarState(
const global = useGlobal() const global = useGlobal()
const notification = useNotification() const notification = useNotification()
const permission = usePermission() const permission = usePermission()
const permissionState = createMemo(() => permission.ensureServerState(server()))
const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server()))
const sync = createMemo(() => { const sync = createMemo(() => {
const conn = connection() const conn = connection()
@@ -22,9 +21,10 @@ export function useSessionTabAvatarState(
const hasPermissions = createMemo(() => { const hasPermissions = createMemo(() => {
const serverSync = sync() const serverSync = sync()
if (!serverSync) return false if (!serverSync) return false
const permissionState = permission.ensureServerState(server())
const [store] = serverSync.child(directory(), { bootstrap: false }) const [store] = serverSync.child(directory(), { bootstrap: false })
return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => {
return !permissionState().autoResponds(item, directory()) return !permissionState.autoResponds(item, directory())
}) })
}) })
const hasQuestions = createMemo(() => { const hasQuestions = createMemo(() => {
@@ -34,9 +34,11 @@ export function useSessionTabAvatarState(
return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId())
}) })
const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) const needsAttention = createMemo(() => hasPermissions() || hasQuestions())
const unread = createMemo( const notificationState = createMemo(() => {
() => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, if (!connection()) return
) return notification.ensureServerState(server())
})
const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0)
const loading = createMemo(() => { const loading = createMemo(() => {
const serverSync = sync() const serverSync = sync()
if (!serverSync) return false if (!serverSync) return false
+8 -14
View File
@@ -25,7 +25,7 @@ import { createPromptInputController, createPromptProjectControls } from "@/page
import { useSessionKey } from "@/pages/session/session-layout" import { useSessionKey } from "@/pages/session/session-layout"
import { useComposerCommands } from "@/pages/session/use-composer-commands" import { useComposerCommands } from "@/pages/session/use-composer-commands"
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector"
import { useTitlebarRightMount } from "@/components/titlebar" import { useTitlebarRightMount } from "@/components/titlebar"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
@@ -168,24 +168,18 @@ export default function NewSessionPage() {
<div class="flex-1 min-h-0 overflow-hidden rounded-[10px]"> <div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
<NewSessionDesignView> <NewSessionDesignView>
<div class={NEW_SESSION_CONTENT_WIDTH}> <div class={NEW_SESSION_CONTENT_WIDTH}>
<div class="flex flex-col" classList={{ "gap-8": showWorkspaceBar(), "gap-3": !showWorkspaceBar() }}> <div class="flex flex-col gap-8">
<PromptInputV2Composer controller={promptInputV2Controller} /> <PromptInputV2Composer controller={promptInputV2Controller} />
<Show when={projectController.empty()}> <Show when={projectController.empty()}>
<PromptProjectAddButton controller={projectController} /> <PromptProjectAddButton controller={projectController} />
</Show> </Show>
<Show when={projectController.selected()}> <Show when={projectController.selected()}>
<div <div class="flex min-h-7 min-w-0 flex-col items-center justify-center gap-0 text-v2-text-text-faint sm:flex-row">
class="flex min-h-7 min-w-0 items-center gap-0 text-v2-text-text-faint" <PromptProjectSelector controller={projectController} placement="bottom" />
classList={{ <Show
"flex-col justify-center sm:flex-row": showWorkspaceBar(), when={showWorkspaceBar()}
"justify-start": !showWorkspaceBar(), fallback={<PromptGitStatus branch={selectedBranch()} noGit={sync().project?.vcs !== "git"} />}
}} >
>
<PromptProjectSelector
controller={projectController}
placement={showWorkspaceBar() ? "bottom" : "bottom-start"}
/>
<Show when={showWorkspaceBar()}>
<PromptWorkspaceSelector <PromptWorkspaceSelector
value={newSessionWorktree()} value={newSessionWorktree()}
projectRoot={projectRoot()} projectRoot={projectRoot()}
+10 -17
View File
@@ -92,7 +92,6 @@ import { TerminalPanel } from "@/pages/session/terminal-panel"
import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2" import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2"
import { useComposerCommands } from "@/pages/session/use-composer-commands" import { useComposerCommands } from "@/pages/session/use-composer-commands"
import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionCommands } from "@/pages/session/use-session-commands"
import { supportsSessionHistory } from "@/pages/session/session-history-actions"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { Identifier } from "@/utils/id" import { Identifier } from "@/utils/id"
import { diffs as list } from "@/utils/diffs" import { diffs as list } from "@/utils/diffs"
@@ -651,7 +650,6 @@ export default function Page() {
}, desktopReviewOpen()) }, desktopReviewOpen())
const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs)) const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs))
const git = createMemo(() => supportsSessionHistory(sync().project?.vcs))
const nogit = createMemo(() => { const nogit = createMemo(() => {
const project = sync().project const project = sync().project
return !!project && project.vcs !== "git" return !!project && project.vcs !== "git"
@@ -670,7 +668,8 @@ export default function Page() {
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
const wantsReview = createMemo(() => const wantsReview = createMemo(() =>
isDesktop() isDesktop()
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") ? desktopFileTreeOpen() ||
(desktopReviewOpen() && (activeTab() === "review" || (newSessionDesign() && !!activeFileTab())))
: store.mobileTab === "changes", : store.mobileTab === "changes",
) )
const vcsMode = createMemo<VcsMode | undefined>(() => { const vcsMode = createMemo<VcsMode | undefined>(() => {
@@ -1917,12 +1916,12 @@ export default function Page() {
const restoring = createMemo(() => (restoreMutation.isPending ? restoreMutation.variables : undefined)) const restoring = createMemo(() => (restoreMutation.isPending ? restoreMutation.variables : undefined))
const revert = (input: { sessionID: string; messageID: string }) => { const revert = (input: { sessionID: string; messageID: string }) => {
if (reverting() || !git()) return if (reverting()) return
return revertMutation.mutateAsync(input) return revertMutation.mutateAsync(input)
} }
const restore = (id: string) => { const restore = (id: string) => {
if (!params.id || reverting() || !git()) return if (!params.id || reverting()) return
return restoreMutation.mutateAsync(id) return restoreMutation.mutateAsync(id)
} }
@@ -1957,16 +1956,7 @@ export default function Page() {
download() download()
} }
const actions = { const actions = { revert, openAttachment }
revert,
get revertDisabled() {
return !git()
},
get revertDisabledReason() {
return nogit() ? language.t("command.session.undo.gitRequired") : undefined
},
openAttachment,
}
createEffect(() => { createEffect(() => {
const sessionID = params.id const sessionID = params.id
@@ -2198,8 +2188,7 @@ export default function Page() {
? { ? {
items: rolled(), items: rolled(),
restoring: restoring(), restoring: restoring(),
disabled: reverting() || !git(), disabled: reverting(),
disabledReason: nogit() ? language.t("command.session.undo.gitRequired") : undefined,
onRestore: restore, onRestore: restore,
} }
: undefined, : undefined,
@@ -2277,6 +2266,7 @@ export default function Page() {
return ( return (
<PromptInputV2Composer <PromptInputV2Composer
controller={controller} controller={controller}
borderUnderlay
edit={editingFollowup()} edit={editingFollowup()}
onEditLoaded={clearFollowupEdit} onEditLoaded={clearFollowupEdit}
/> />
@@ -2377,6 +2367,9 @@ export default function Page() {
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()} reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
reviewCount={reviewCount} reviewCount={reviewCount}
reviewPanel={reviewPanelV2} reviewPanel={reviewPanelV2}
diffVersion={vcsQuery.dataUpdatedAt}
loadDiff={loadReviewDiff}
expandUnchanged={reviewV2State.expandMode() === "expand"}
reviewSidebarToggle={(disabled) => ( reviewSidebarToggle={(disabled) => (
<SessionReviewV2SidebarToggle <SessionReviewV2SidebarToggle
opened={reviewV2State.sidebarOpened()} opened={reviewV2State.sidebarOpened()}
@@ -18,7 +18,6 @@ export type SessionComposerRevertDock = {
items: { id: string; text: string }[] items: { id: string; text: string }[]
restoring?: string restoring?: string
disabled?: boolean disabled?: boolean
disabledReason?: string
onRestore: (id: string) => void onRestore: (id: string) => void
} }
@@ -93,7 +93,6 @@ export function SessionComposerRegion(props: {
items={revert.items} items={revert.items}
restoring={revert.restoring} restoring={revert.restoring}
disabled={revert.disabled} disabled={revert.disabled}
disabledReason={revert.disabledReason}
onRestore={revert.onRestore} onRestore={revert.onRestore}
/> />
</div> </div>
@@ -119,7 +118,6 @@ export function SessionComposerRegion(props: {
items={revert.items} items={revert.items}
restoring={revert.restoring} restoring={revert.restoring}
disabled={revert.disabled} disabled={revert.disabled}
disabledReason={revert.disabledReason}
onRestore={revert.onRestore} onRestore={revert.onRestore}
/> />
</div> </div>
@@ -1,5 +1,6 @@
import { For } from "solid-js" import { For } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { DockShell } from "@opencode-ai/ui/dock-surface"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import { SettingsProvider, useSettings } from "@/context/settings" import { SettingsProvider, useSettings } from "@/context/settings"
@@ -78,12 +79,17 @@ function Stage(props: { count: number }) {
{/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */} {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
<div style={{ display: "flex", "flex-direction": "column" }}> <div style={{ display: "flex", "flex-direction": "column" }}>
<SessionRevertDock items={store.items} onRestore={restore} /> <SessionRevertDock items={store.items} onRestore={restore} />
<div <DockShell
data-dock-border-underlay={v2() ? "v2" : "legacy"}
style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }} style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }}
class="min-h-24 w-full rounded-[12px] border border-v2-border-border-base bg-v2-background-bg-base px-4 py-3 text-[13px] text-v2-text-text-faint" classList={{
"min-h-24 w-full rounded-[12px] px-4 py-3 text-[13px]": true,
"bg-v2-background-bg-base text-v2-text-text-faint": v2(),
"text-text-weak": !v2(),
}}
> >
Ask anything... Ask anything...
</div> </DockShell>
</div> </div>
<div class="text-[12px] text-v2-text-text-faint"> <div class="text-[12px] text-v2-text-text-faint">
@@ -6,8 +6,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
@@ -15,7 +13,6 @@ export function SessionRevertDock(props: {
items: { id: string; text: string }[] items: { id: string; text: string }[]
restoring?: string restoring?: string
disabled?: boolean disabled?: boolean
disabledReason?: string
onRestore: (id: string) => void onRestore: (id: string) => void
}) { }) {
const language = useLanguage() const language = useLanguage()
@@ -92,17 +89,15 @@ export function SessionRevertDock(props: {
{(item) => ( {(item) => (
<div class="flex items-center gap-2 min-w-0 py-1"> <div class="flex items-center gap-2 min-w-0 py-1">
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span> <span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
<Tooltip inactive={!props.disabledReason} value={props.disabledReason} placement="top"> <Button
<Button size="small"
size="small" variant="secondary"
variant="secondary" class="shrink-0"
class="shrink-0" disabled={props.disabled || !!props.restoring}
disabled={props.disabled || !!props.restoring} onClick={() => props.onRestore(item.id)}
onClick={() => props.onRestore(item.id)} >
> {language.t("session.revertDock.restore")}
{language.t("session.revertDock.restore")} </Button>
</Button>
</Tooltip>
</div> </div>
)} )}
</For> </For>
@@ -172,17 +167,15 @@ export function SessionRevertDock(props: {
<span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted"> <span class="min-w-0 flex-1 truncate text-[13px] font-[400] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
{item.text} {item.text}
</span> </span>
<TooltipV2 inactive={!props.disabledReason} value={props.disabledReason} placement="top"> <ButtonV2
<ButtonV2 size="small"
size="small" variant="neutral"
variant="neutral" class="shrink-0"
class="shrink-0" disabled={props.disabled || !!props.restoring}
disabled={props.disabled || !!props.restoring} onClick={() => props.onRestore(item.id)}
onClick={() => props.onRestore(item.id)} >
> {language.t("session.revertDock.restore")}
{language.t("session.revertDock.restore")} </ButtonV2>
</ButtonV2>
</TooltipV2>
</div> </div>
)} )}
</For> </For>
@@ -8,8 +8,10 @@ import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { Index, createEffect, createMemo } from "solid-js" import { Index, createEffect, createMemo } from "solid-js"
import { Dynamic } from "solid-js/web"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
const doneToken = "\u0000done\u0000" const doneToken = "\u0000done\u0000"
const totalToken = "\u0000total\u0000" const totalToken = "\u0000total\u0000"
@@ -48,6 +50,7 @@ export function SessionTodoDock(props: {
dockProgress: number dockProgress: number
}) { }) {
const language = useLanguage() const language = useLanguage()
const settings = useSettings()
const [store, setStore] = createStore({ const [store, setStore] = createStore({
height: 78, height: 78,
}) })
@@ -91,8 +94,13 @@ export function SessionTodoDock(props: {
}) })
return ( return (
<DockTray <Dynamic
component={settings.general.newLayoutDesigns() ? "div" : DockTray}
data-component="session-todo-dock" data-component="session-todo-dock"
classList={{
"w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01":
settings.general.newLayoutDesigns(),
}}
style={{ style={{
"overflow-x": "visible", "overflow-x": "visible",
"overflow-y": "hidden", "overflow-y": "hidden",
@@ -102,7 +110,11 @@ export function SessionTodoDock(props: {
<div ref={contentRef}> <div ref={contentRef}>
<div <div
data-action="session-todo-toggle" data-action="session-todo-toggle"
class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible" classList={{
"flex items-center gap-2 overflow-visible": true,
"h-[42px] pl-4 pr-2": settings.general.newLayoutDesigns(),
"pl-3 pr-2 py-2": !settings.general.newLayoutDesigns(),
}}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={props.onToggle} onClick={props.onToggle}
@@ -113,7 +125,12 @@ export function SessionTodoDock(props: {
}} }}
> >
<span <span
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 overflow-visible" classList={{
"cursor-default inline-flex items-baseline shrink-0 overflow-visible": true,
"font-[440] text-[13px] leading-5 tracking-[-0.04px] text-v2-text-text-muted":
settings.general.newLayoutDesigns(),
"text-14-regular text-text-strong": !settings.general.newLayoutDesigns(),
}}
aria-label={label()} aria-label={label()}
style={{ style={{
"--tool-motion-odometer-ms": "600ms", "--tool-motion-odometer-ms": "600ms",
@@ -145,7 +162,11 @@ export function SessionTodoDock(props: {
}} }}
> >
<TextReveal <TextReveal
class="text-14-regular text-text-base cursor-default" class={
settings.general.newLayoutDesigns()
? "cursor-default text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint"
: "text-14-regular text-text-base cursor-default"
}
text={props.collapsed ? preview() : undefined} text={props.collapsed ? preview() : undefined}
duration={600} duration={600}
travel={25} travel={25}
@@ -191,7 +212,7 @@ export function SessionTodoDock(props: {
<TodoList todos={props.todos} /> <TodoList todos={props.todos} />
</div> </div>
</div> </div>
</DockTray> </Dynamic>
) )
} }
+62 -16
View File
@@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js" import { createEffect, createMemo, createResource, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
@@ -8,6 +8,7 @@ import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/sessi
import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations" import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations"
import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2" import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2"
import { sampledChecksum } from "@opencode-ai/core/util/encode" import { sampledChecksum } from "@opencode-ai/core/util/encode"
import { normalize, text } from "@opencode-ai/session-ui/session-diff"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
@@ -18,11 +19,23 @@ import { showToast } from "@/utils/toast"
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { getSessionHandoff } from "@/pages/session/handoff" import { getSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers" import { createSessionTabs } from "@/pages/session/helpers"
import { reviewDiffNeedsLoad, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
type SessionFileViewProps = {
tab: string
diff?: RenderDiff
diffVersion?: number
loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
expandUnchanged?: boolean
}
const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions"
function FileCommentMenu(props: { function FileCommentMenu(props: {
moreLabel: string moreLabel: string
@@ -207,11 +220,27 @@ export function FileTabContent(props: { tab: string }) {
) )
} }
export function SessionFileView(props: { tab: string }) { export function SessionFileView(props: SessionFileViewProps) {
const settings = useSettings() const settings = useSettings()
const detailSource = createMemo(() => {
if (!props.diff || !props.loadDiff || !reviewDiffNeedsLoad(props.diff)) return
return { diff: props.diff, load: props.loadDiff, version: props.diffVersion }
})
const [loadedDiff] = createResource(detailSource, async ({ diff, load, version }) => {
const value = await load(diff.file, version)
if (value?.file !== diff.file) return
return { source: diff, version, value }
})
const diff = createMemo(() => {
const source = props.diff
if (!source) return
const loaded = loadedDiff()
return normalize(loaded?.source === source && loaded.version === props.diffVersion ? loaded.value : source)
})
return ( return (
<Show when={settings.general.newLayoutDesigns()} fallback={<SessionFileViewV1 tab={props.tab} />}> <Show when={settings.general.newLayoutDesigns()} fallback={<SessionFileViewV1 tab={props.tab} />}>
<SessionFileViewV2 tab={props.tab} /> <SessionFileViewV2 tab={props.tab} diff={diff()} expandUnchanged={props.expandUnchanged} />
</Show> </Show>
) )
} }
@@ -501,11 +530,12 @@ function SessionFileViewV1(props: { tab: string }) {
return content() return content()
} }
function SessionFileViewV2(props: { tab: string }) { function SessionFileViewV2(props: { tab: string; diff?: ReturnType<typeof normalize>; expandUnchanged?: boolean }) {
const file = useFile() const file = useFile()
const comments = useComments() const comments = useComments()
const language = useLanguage() const language = useLanguage()
const prompt = usePrompt() const prompt = usePrompt()
const layout = useLayout()
const fileComponent = useFileComponent() const fileComponent = useFileComponent()
const { sessionKey, tabs, view } = useSessionLayout() const { sessionKey, tabs, view } = useSessionLayout()
const activeFileTab = createSessionTabs({ const activeFileTab = createSessionTabs({
@@ -548,10 +578,15 @@ function SessionFileViewV2(props: { tab: string }) {
}) })
} }
const buildPreview = (filePath: string, selection: FileSelection) => { const buildPreview = (filePath: string, lines: SelectedLineRange) => {
const source = filePath === path() ? contents() : file.get(filePath)?.content?.content const source =
filePath === path()
? props.diff
? text(props.diff, selectionSide(lines))
: contents()
: file.get(filePath)?.content?.content
if (!source) return undefined if (!source) return undefined
return selectionPreview(source, selection) return selectionPreview(source, selectionFromLines(lines))
} }
const addCommentToContext = (input: { const addCommentToContext = (input: {
@@ -562,7 +597,7 @@ function SessionFileViewV2(props: { tab: string }) {
origin?: "review" | "file" origin?: "review" | "file"
}) => { }) => {
const selection = selectionFromLines(input.selection) const selection = selectionFromLines(input.selection)
const preview = input.preview ?? buildPreview(input.file, selection) const preview = input.preview ?? buildPreview(input.file, input.selection)
const saved = comments.add({ const saved = comments.add({
file: input.file, file: input.file,
@@ -587,7 +622,7 @@ function SessionFileViewV2(props: { tab: string }) {
comment: string comment: string
}) => { }) => {
comments.update(input.file, input.id, input.comment) comments.update(input.file, input.id, input.comment)
const preview = input.file === path() ? buildPreview(input.file, selectionFromLines(input.selection)) : undefined const preview = input.file === path() ? buildPreview(input.file, input.selection) : undefined
prompt.context.updateComment(input.file, input.id, { prompt.context.updateComment(input.file, input.id, {
comment: input.comment, comment: input.comment,
...(preview ? { preview } : {}), ...(preview ? { preview } : {}),
@@ -628,7 +663,7 @@ function SessionFileViewV2(props: { tab: string }) {
mention: { mention: {
items: file.searchFilesAndDirectories, items: file.searchFilesAndDirectories,
}, },
getSide: (range) => range.endSide ?? range.side ?? "additions", getSide: selectionSide,
state: { state: {
opened: () => note.openedComment, opened: () => note.openedComment,
setOpened: (id) => setNote("openedComment", id), setOpened: (id) => setNote("openedComment", id),
@@ -726,12 +761,21 @@ function SessionFileViewV2(props: { tab: string }) {
<div class="relative overflow-hidden pb-40"> <div class="relative overflow-hidden pb-40">
<Dynamic <Dynamic
component={fileComponent} component={fileComponent}
mode="text" {...(props.diff
file={{ ? {
name: path() ?? "", mode: "diff" as const,
contents: source, fileDiff: props.diff.fileDiff,
cacheKey: cacheKey(), diffStyle: layout.review.diffStyle(),
}} expandUnchanged: props.expandUnchanged,
}
: {
mode: "text" as const,
file: {
name: path() ?? "",
contents: source,
cacheKey: cacheKey(),
},
})}
enableLineSelection enableLineSelection
enableGutterUtility enableGutterUtility
selectedLines={activeSelection()} selectedLines={activeSelection()}
@@ -761,6 +805,7 @@ function SessionFileViewV2(props: { tab: string }) {
media={{ media={{
mode: "auto", mode: "auto",
path: path(), path: path(),
deleted: props.diff?.status === "deleted",
current: state()?.content, current: state()?.content,
onLoad: scrollSync.queueRestore, onLoad: scrollSync.queueRestore,
onError: (args: { kind: "image" | "audio" | "svg" }) => { onError: (args: { kind: "image" | "audio" | "svg" }) => {
@@ -779,6 +824,7 @@ function SessionFileViewV2(props: { tab: string }) {
<div class="mt-3 relative h-full min-h-0"> <div class="mt-3 relative h-full min-h-0">
<ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}> <ScrollView class="h-full" viewportRef={scrollSync.setViewport} onScroll={scrollSync.handleScroll as any}>
<Switch> <Switch>
<Match when={props.diff}>{renderFile(contents())}</Match>
<Match when={state()?.loaded}>{renderFile(contents())}</Match> <Match when={state()?.loaded}>{renderFile(contents())}</Match>
<Match when={state()?.loading}> <Match when={state()?.loading}>
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div> <div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
@@ -1,28 +0,0 @@
import { describe, expect, test } from "bun:test"
import { commandPaletteOptions } from "@/context/command"
import { sessionHistoryActions } from "./session-history-actions"
describe("sessionHistoryActions", () => {
test("enables session history actions for Git projects", () => {
expect(
sessionHistoryActions({
vcs: "git",
sessionID: "session-1",
hasVisibleUserMessage: true,
hasRevert: true,
}),
).toEqual({ undo: true, redo: true, revert: true })
})
test("disables session history actions for non-Git projects", () => {
const actions = sessionHistoryActions({
vcs: undefined,
sessionID: "session-1",
hasVisibleUserMessage: true,
hasRevert: true,
})
expect(actions).toEqual({ undo: false, redo: false, revert: false })
expect(commandPaletteOptions([{ id: "session.undo", title: "Undo", disabled: !actions.undo }])).toEqual([])
})
})
@@ -1,17 +0,0 @@
export function supportsSessionHistory(vcs?: string) {
return vcs === "git"
}
export function sessionHistoryActions(input: {
vcs?: string
sessionID?: string
hasVisibleUserMessage: boolean
hasRevert: boolean
}) {
const git = supportsSessionHistory(input.vcs)
return {
undo: git && !!input.sessionID && input.hasVisibleUserMessage,
redo: git && !!input.sessionID && input.hasRevert,
revert: git,
}
}
@@ -71,6 +71,9 @@ export function SessionSidePanel(props: {
reviewHasFocusableContent: () => boolean reviewHasFocusableContent: () => boolean
reviewCount: () => number reviewCount: () => number
reviewPanel: () => JSX.Element reviewPanel: () => JSX.Element
diffVersion?: number
loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
expandUnchanged?: boolean
reviewSidebarToggle?: (disabled: boolean) => JSX.Element reviewSidebarToggle?: (disabled: boolean) => JSX.Element
fileBrowserState?: SessionFileBrowserState fileBrowserState?: SessionFileBrowserState
activeDiff?: string activeDiff?: string
@@ -88,6 +91,11 @@ export function SessionSidePanel(props: {
const sdk = useSDK() const sdk = useSDK()
const { sessionKey, tabs, view, params } = useSessionLayout() const { sessionKey, tabs, view, params } = useSessionLayout()
const projectDirectory = createMemo(() => sdk().directory) const projectDirectory = createMemo(() => sdk().directory)
const diffForTab = (tab: string) => {
const path = file.pathFromTab(tab)
if (!path) return
return props.diffs().find((diff): diff is RenderDiff => renderDiff(diff) && diff.file === path)
}
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const shown = settings.visibility.fileTree const shown = settings.visibility.fileTree
@@ -739,6 +747,10 @@ export function SessionSidePanel(props: {
active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")} active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")}
kinds={kinds()} kinds={kinds()}
state={props.fileBrowserState!} state={props.fileBrowserState!}
diff={diffForTab(browserTab() ?? activeFileTab() ?? "")}
diffVersion={props.diffVersion}
loadDiff={props.loadDiff}
expandUnchanged={props.expandUnchanged}
onSelect={(path) => previewTab(file.tab(path))} onSelect={(path) => previewTab(file.tab(path))}
onSelectPermanent={(path) => openTab(file.tab(path))} onSelectPermanent={(path) => openTab(file.tab(path))}
filterRef={(element) => (fileFilter = element)} filterRef={(element) => (fileFilter = element)}
@@ -19,7 +19,6 @@ import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2" import { UserMessage } from "@opencode-ai/sdk/v2"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionOwnership } from "./session-ownership" import { createSessionOwnership } from "./session-ownership"
import { sessionHistoryActions } from "./session-history-actions"
export type SessionCommandContext = { export type SessionCommandContext = {
navigateMessageByOffset: (offset: number) => void navigateMessageByOffset: (offset: number) => void
@@ -102,13 +101,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
if (!revert) return userMessages() if (!revert) return userMessages()
return userMessages().filter((m) => m.id < revert) return userMessages().filter((m) => m.id < revert)
} }
const historyActions = () =>
sessionHistoryActions({
vcs: sync().project?.vcs,
sessionID: params.id,
hasVisibleUserMessage: visibleUserMessages().length > 0,
hasRevert: !!info()?.revert?.messageID,
})
const showAllFiles = () => { const showAllFiles = () => {
if (layout.fileTree.tab() !== "changes") return if (layout.fileTree.tab() !== "changes") return
@@ -311,7 +303,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
} }
const undo = async () => { const undo = async () => {
if (!historyActions().undo) return
const sessionID = params.id const sessionID = params.id
if (!sessionID) return if (!sessionID) return
const owner = sessionOwnership.capture() const owner = sessionOwnership.capture()
@@ -340,7 +331,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
} }
const redo = async () => { const redo = async () => {
if (!historyActions().redo) return
const sessionID = params.id const sessionID = params.id
if (!sessionID) return if (!sessionID) return
const owner = sessionOwnership.capture() const owner = sessionOwnership.capture()
@@ -440,12 +430,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
sessionCommand({ sessionCommand({
id: "session.undo", id: "session.undo",
title: language.t("command.session.undo"), title: language.t("command.session.undo"),
description: description: language.t("command.session.undo.description"),
sync().project && sync().project?.vcs !== "git"
? language.t("command.session.undo.gitRequired")
: language.t("command.session.undo.description"),
slash: "undo", slash: "undo",
disabled: !historyActions().undo, disabled: !params.id || visibleUserMessages().length === 0,
onSelect: undo, onSelect: undo,
}), }),
sessionCommand({ sessionCommand({
@@ -453,7 +440,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
title: language.t("command.session.redo"), title: language.t("command.session.redo"),
description: language.t("command.session.redo.description"), description: language.t("command.session.redo.description"),
slash: "redo", slash: "redo",
disabled: !historyActions().redo, disabled: !params.id || !info()?.revert?.messageID,
onSelect: redo, onSelect: redo,
}), }),
sessionCommand({ sessionCommand({
@@ -11,6 +11,7 @@ import { useSDK } from "@/context/sdk"
import { displayName } from "@/pages/layout/helpers" import { displayName } from "@/pages/layout/helpers"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
import { SessionFileView } from "@/pages/session/file-tabs" import { SessionFileView } from "@/pages/session/file-tabs"
import type { RenderDiff } from "@/pages/session/v2/review-diff-kinds"
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
@@ -30,6 +31,10 @@ export function SessionFileBrowserTab(props: {
active?: string active?: string
kinds: ReadonlyMap<string, Kind> kinds: ReadonlyMap<string, Kind>
state: SessionFileBrowserState state: SessionFileBrowserState
diff?: RenderDiff
diffVersion?: number
loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
expandUnchanged?: boolean
onSelect: (path: string) => void onSelect: (path: string) => void
onSelectPermanent: (path: string) => void onSelectPermanent: (path: string) => void
filterRef?: (element: HTMLInputElement) => void filterRef?: (element: HTMLInputElement) => void
@@ -172,7 +177,15 @@ export function SessionFileBrowserTab(props: {
> >
<div class="min-h-0 flex-1"> <div class="min-h-0 flex-1">
<Show when={props.tab} keyed> <Show when={props.tab} keyed>
{(tab) => <SessionFileView tab={tab} />} {(tab) => (
<SessionFileView
tab={tab}
diff={props.diff}
diffVersion={props.diffVersion}
loadDiff={props.loadDiff}
expandUnchanged={props.expandUnchanged}
/>
)}
</Show> </Show>
</div> </div>
</Show> </Show>
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.18.3", "version": "1.18.4",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
+1 -10
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun" import { $ } from "bun"
import fs from "fs"
import { rm } from "fs/promises" import { rm } from "fs/promises"
import path from "path" import path from "path"
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
@@ -51,10 +50,6 @@ const targets = singleFlag
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
for (const item of targets) { for (const item of targets) {
const target = [ const target = [
binary, binary,
@@ -68,7 +63,7 @@ for (const item of targets) {
const name = target.replace(binary, "cli") const name = target.replace(binary, "cli")
console.log(`building ${name}`) console.log(`building ${name}`)
const result = await Bun.build({ const result = await Bun.build({
entrypoints: ["./src/index.ts", parserWorker], entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json", tsconfig: "./tsconfig.json",
plugins: [plugin], plugins: [plugin],
external: ["node-gyp"], external: ["node-gyp"],
@@ -94,10 +89,6 @@ for (const item of targets) {
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined", OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu". // FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined", FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
OTUI_TREE_SITTER_WORKER_PATH:
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
path.relative(dir, parserWorker).replaceAll("\\", "/") +
'"',
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
}, },
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode", "name": "@opencode-ai/codemode",
"version": "1.18.3", "version": "1.18.4",
"description": "Effect-native confined code execution over schema-described tools", "description": "Effect-native confined code execution over schema-described tools",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.18.3", "version": "1.18.4",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
-2
View File
@@ -9,7 +9,6 @@ import "./app.css"
import { LanguageProvider } from "~/context/language" import { LanguageProvider } from "~/context/language"
import { I18nProvider, useI18n } from "~/context/i18n" import { I18nProvider, useI18n } from "~/context/i18n"
import { strip } from "~/lib/language" import { strip } from "~/lib/language"
import { DesktopPromo } from "~/component/desktop-promo"
function AppMeta() { function AppMeta() {
const i18n = useI18n() const i18n = useI18n()
@@ -34,7 +33,6 @@ export default function App() {
<MetaProvider> <MetaProvider>
<AppMeta /> <AppMeta />
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>
<DesktopPromo />
</MetaProvider> </MetaProvider>
</I18nProvider> </I18nProvider>
</LanguageProvider> </LanguageProvider>
+1
View File
@@ -358,6 +358,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم", "zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم",
"zen.api.error.missingApiKey": "مفتاح API مفقود.", "zen.api.error.missingApiKey": "مفتاح API مفقود.",
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.", "zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
"zen.api.error.requestBlockedByUpstreamProvider": "حظر المزود الخارجي الطلب.",
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}", "تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
+1
View File
@@ -366,6 +366,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado", "zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado",
"zen.api.error.missingApiKey": "Chave de API ausente.", "zen.api.error.missingApiKey": "Chave de API ausente.",
"zen.api.error.invalidApiKey": "Chave de API inválida.", "zen.api.error.invalidApiKey": "Chave de API inválida.",
"zen.api.error.requestBlockedByUpstreamProvider": "Solicitação bloqueada pelo provedor upstream.",
"zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Limite de uso de 5 horas atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}", "Limite de uso de 5 horas atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}",
+1
View File
@@ -362,6 +362,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke", "zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke",
"zen.api.error.missingApiKey": "Manglende API-nøgle.", "zen.api.error.missingApiKey": "Manglende API-nøgle.",
"zen.api.error.invalidApiKey": "Ugyldig API-nøgle.", "zen.api.error.invalidApiKey": "Ugyldig API-nøgle.",
"zen.api.error.requestBlockedByUpstreamProvider": "Anmodningen blev blokeret af upstream-udbyderen.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Forbrugsgrænsen for 5 timer er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}", "Forbrugsgrænsen for 5 timer er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}",
+1
View File
@@ -365,6 +365,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt", "zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt",
"zen.api.error.missingApiKey": "Fehlender API-Key.", "zen.api.error.missingApiKey": "Fehlender API-Key.",
"zen.api.error.invalidApiKey": "Ungültiger API-Key.", "zen.api.error.invalidApiKey": "Ungültiger API-Key.",
"zen.api.error.requestBlockedByUpstreamProvider": "Anfrage vom vorgelagerten Anbieter blockiert.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"5-Stunden-Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}", "5-Stunden-Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}",
+5
View File
@@ -360,6 +360,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Provider {{provider}} not supported", "zen.api.error.providerNotSupported": "Provider {{provider}} not supported",
"zen.api.error.missingApiKey": "Missing API key.", "zen.api.error.missingApiKey": "Missing API key.",
"zen.api.error.invalidApiKey": "Invalid API key.", "zen.api.error.invalidApiKey": "Invalid API key.",
"zen.api.error.requestBlockedByUpstreamProvider": "Request blocked by upstream provider.",
"zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", "5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}",
@@ -777,6 +778,10 @@ export const dict = {
"enterprise.faq.q4": "Is my data secure with OpenCode Enterprise?", "enterprise.faq.q4": "Is my data secure with OpenCode Enterprise?",
"enterprise.faq.a4": "enterprise.faq.a4":
"Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.", "Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.",
"enterprise.faq.q5": "Where can I find your security and compliance documentation?",
"enterprise.faq.a5.before":
"Our Trust Center has everything: SOC 2 Type 2 report, security policies, subprocessor list, and answers to common security questions. Visit",
"enterprise.faq.a5.after": "to review or request documents under NDA.",
"brand.title": "OpenCode | Brand", "brand.title": "OpenCode | Brand",
"brand.meta.description": "OpenCode brand guidelines", "brand.meta.description": "OpenCode brand guidelines",
+1
View File
@@ -366,6 +366,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado", "zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado",
"zen.api.error.missingApiKey": "Falta la clave API.", "zen.api.error.missingApiKey": "Falta la clave API.",
"zen.api.error.invalidApiKey": "Clave API inválida.", "zen.api.error.invalidApiKey": "Clave API inválida.",
"zen.api.error.requestBlockedByUpstreamProvider": "El proveedor externo bloqueó la solicitud.",
"zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Límite de uso de 5 horas alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}", "Límite de uso de 5 horas alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}",
+1
View File
@@ -366,6 +366,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge", "zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge",
"zen.api.error.missingApiKey": "Clé API manquante.", "zen.api.error.missingApiKey": "Clé API manquante.",
"zen.api.error.invalidApiKey": "Clé API invalide.", "zen.api.error.invalidApiKey": "Clé API invalide.",
"zen.api.error.requestBlockedByUpstreamProvider": "Requête bloquée par le fournisseur en amont.",
"zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Limite d'utilisation sur 5 heures atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}", "Limite d'utilisation sur 5 heures atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}",
+1
View File
@@ -362,6 +362,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Provider {{provider}} non supportato", "zen.api.error.providerNotSupported": "Provider {{provider}} non supportato",
"zen.api.error.missingApiKey": "Chiave API mancante.", "zen.api.error.missingApiKey": "Chiave API mancante.",
"zen.api.error.invalidApiKey": "Chiave API non valida.", "zen.api.error.invalidApiKey": "Chiave API non valida.",
"zen.api.error.requestBlockedByUpstreamProvider": "Richiesta bloccata dal provider upstream.",
"zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Limite di utilizzo di 5 ore raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}", "Limite di utilizzo di 5 ore raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}",
+1
View File
@@ -362,6 +362,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません", "zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません",
"zen.api.error.missingApiKey": "APIキーがありません。", "zen.api.error.missingApiKey": "APIキーがありません。",
"zen.api.error.invalidApiKey": "無効なAPIキーです。", "zen.api.error.invalidApiKey": "無効なAPIキーです。",
"zen.api.error.requestBlockedByUpstreamProvider": "上流プロバイダーによりリクエストがブロックされました。",
"zen.api.error.subscriptionQuotaExceeded": "zen.api.error.subscriptionQuotaExceeded":
"サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。", "サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
+1
View File
@@ -357,6 +357,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다", "zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다",
"zen.api.error.missingApiKey": "API 키가 누락되었습니다.", "zen.api.error.missingApiKey": "API 키가 누락되었습니다.",
"zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.", "zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.",
"zen.api.error.requestBlockedByUpstreamProvider": "상위 제공자가 요청을 차단했습니다.",
"zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.", "zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"5시간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}", "5시간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}",
+1
View File
@@ -363,6 +363,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke", "zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke",
"zen.api.error.missingApiKey": "Mangler API-nøkkel.", "zen.api.error.missingApiKey": "Mangler API-nøkkel.",
"zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.", "zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.",
"zen.api.error.requestBlockedByUpstreamProvider": "Forespørselen ble blokkert av leverandøren.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"5-timers bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}", "5-timers bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}",
+1
View File
@@ -364,6 +364,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany", "zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany",
"zen.api.error.missingApiKey": "Brak klucza API.", "zen.api.error.missingApiKey": "Brak klucza API.",
"zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.", "zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.",
"zen.api.error.requestBlockedByUpstreamProvider": "Żądanie zablokowane przez dostawcę zewnętrznego.",
"zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Osiągnięto 5-godzinny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}", "Osiągnięto 5-godzinny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}",
+1
View File
@@ -368,6 +368,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается", "zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается",
"zen.api.error.missingApiKey": "Отсутствует API ключ.", "zen.api.error.missingApiKey": "Отсутствует API ключ.",
"zen.api.error.invalidApiKey": "Неверный API ключ.", "zen.api.error.invalidApiKey": "Неверный API ключ.",
"zen.api.error.requestBlockedByUpstreamProvider": "Запрос заблокирован вышестоящим провайдером.",
"zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Достигнут лимит использования за 5 часов. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}", "Достигнут лимит использования за 5 часов. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}",
+1
View File
@@ -359,6 +359,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}", "zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}",
"zen.api.error.missingApiKey": "ไม่มี API key", "zen.api.error.missingApiKey": "ไม่มี API key",
"zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง", "zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง",
"zen.api.error.requestBlockedByUpstreamProvider": "คำขอถูกบล็อกโดยผู้ให้บริการต้นทาง",
"zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}", "zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"ถึงขีดจำกัดการใช้งานในรอบ 5 ชั่วโมงแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}", "ถึงขีดจำกัดการใช้งานในรอบ 5 ชั่วโมงแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}",
+1
View File
@@ -366,6 +366,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor", "zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor",
"zen.api.error.missingApiKey": "API anahtarı eksik.", "zen.api.error.missingApiKey": "API anahtarı eksik.",
"zen.api.error.invalidApiKey": "Geçersiz API anahtarı.", "zen.api.error.invalidApiKey": "Geçersiz API anahtarı.",
"zen.api.error.requestBlockedByUpstreamProvider": "İstek üst sağlayıcı tarafından engellendi.",
"zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.", "zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"5 saatlik kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}", "5 saatlik kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}",
+1
View File
@@ -363,6 +363,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "Провайдер {{provider}} не підтримується", "zen.api.error.providerNotSupported": "Провайдер {{provider}} не підтримується",
"zen.api.error.missingApiKey": "Відсутній ключ API.", "zen.api.error.missingApiKey": "Відсутній ключ API.",
"zen.api.error.invalidApiKey": "Недійсний ключ API.", "zen.api.error.invalidApiKey": "Недійсний ключ API.",
"zen.api.error.requestBlockedByUpstreamProvider": "Запит заблоковано зовнішнім провайдером.",
"zen.api.error.subscriptionQuotaExceeded": "Перевищено квоту підписки. Повторіть через {{retryIn}}.", "zen.api.error.subscriptionQuotaExceeded": "Перевищено квоту підписки. Повторіть через {{retryIn}}.",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"Досягнуто 5-годинного ліміту використання. Скидається через {{retryIn}}. Щоб продовжити, увімкніть використання з доступного балансу: {{consoleGoUrl}}", "Досягнуто 5-годинного ліміту використання. Скидається через {{retryIn}}. Щоб продовжити, увімкніть використання з доступного балансу: {{consoleGoUrl}}",
+1
View File
@@ -346,6 +346,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "不支持提供商 {{provider}}", "zen.api.error.providerNotSupported": "不支持提供商 {{provider}}",
"zen.api.error.missingApiKey": "缺少 API 密钥。", "zen.api.error.missingApiKey": "缺少 API 密钥。",
"zen.api.error.invalidApiKey": "无效的 API 密钥。", "zen.api.error.invalidApiKey": "无效的 API 密钥。",
"zen.api.error.requestBlockedByUpstreamProvider": "请求被上游提供商阻止。",
"zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。", "zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"已达到 5 小时使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}", "已达到 5 小时使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}",
+1
View File
@@ -346,6 +346,7 @@ export const dict = {
"zen.api.error.providerNotSupported": "不支援供應商 {{provider}}", "zen.api.error.providerNotSupported": "不支援供應商 {{provider}}",
"zen.api.error.missingApiKey": "缺少 API 金鑰。", "zen.api.error.missingApiKey": "缺少 API 金鑰。",
"zen.api.error.invalidApiKey": "無效的 API 金鑰。", "zen.api.error.invalidApiKey": "無效的 API 金鑰。",
"zen.api.error.requestBlockedByUpstreamProvider": "請求遭上游供應商封鎖。",
"zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。", "zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。",
"zen.api.error.goSubscriptionRollingLimitExceeded": "zen.api.error.goSubscriptionRollingLimitExceeded":
"已達 5 小時使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}", "已達 5 小時使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}",
@@ -273,6 +273,12 @@ export default function Enterprise() {
<li> <li>
<Faq question={i18n.t("enterprise.faq.q4")}>{i18n.t("enterprise.faq.a4")}</Faq> <Faq question={i18n.t("enterprise.faq.q4")}>{i18n.t("enterprise.faq.a4")}</Faq>
</li> </li>
<li>
<Faq question={i18n.t("enterprise.faq.q5")}>
{i18n.t("enterprise.faq.a5.before")} <a href="https://trust.opencode.ai">trust.opencode.ai</a>{" "}
{i18n.t("enterprise.faq.a5.after")}
</Faq>
</li>
</ul> </ul>
</section> </section>
</div> </div>
+2 -2
View File
@@ -63,8 +63,8 @@ function LimitsGraph(props: { href: string }) {
const baseline = 100 const baseline = 100
const graph = [ const graph = [
{ id: "grok-4.5", name: "Grok 4.5", req: 80, d: "50ms" }, { id: "grok-4.5", name: "Grok 4.5", req: 120, d: "50ms" },
{ id: "kimi-k3", name: "Kimi K3 (2x usage)", req: 280, baseReq: 140, d: "75ms" }, { id: "kimi-k3", name: "Kimi K3 (2x usage)", req: 220, baseReq: 110, d: "75ms" },
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" }, { id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
{ id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" }, { id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" }, { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" },
@@ -0,0 +1,14 @@
import type { APIEvent } from "@solidjs/start/server"
import { handler } from "~/routes/zen/util/handler"
import { parseOpenAiVariant } from "~/routes/zen/util/variant"
export function POST(input: APIEvent) {
return handler(input, {
format: "openai",
modelList: "lite",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
parseModel: (url: string, body: any) => body.model,
parseVariant: (url: string, body: any) => parseOpenAiVariant(body),
parseIsStream: (url: string, body: any) => !!body.stream,
})
}
@@ -697,6 +697,9 @@ export async function handler(
workspace: { workspace: {
id: WorkspaceTable.id, id: WorkspaceTable.id,
region: WorkspaceTable.region, region: WorkspaceTable.region,
isBlocked: WorkspaceTable.is_blocked,
isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic,
isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai,
}, },
billing: { billing: {
balance: BillingTable.balance, balance: BillingTable.balance,
@@ -772,6 +775,12 @@ export async function handler(
) )
if (!data) throw new AuthError(t("zen.api.error.invalidApiKey")) if (!data) throw new AuthError(t("zen.api.error.invalidApiKey"))
if (
data.workspace.isBlocked ||
(data.workspace.isFlaggedByAnthropic && modelInfo.id.startsWith("claude-")) ||
(data.workspace.isFlaggedByOpenAI && modelInfo.id.startsWith("gpt-"))
)
throw new AuthError(t("zen.api.error.requestBlockedByUpstreamProvider"))
if ( if (
modelInfo.id.startsWith("alpha-") && modelInfo.id.startsWith("alpha-") &&
Resource.App.stage === "production" && Resource.App.stage === "production" &&
@@ -0,0 +1,3 @@
ALTER TABLE `workspace` ADD `is_blocked` boolean;--> statement-breakpoint
ALTER TABLE `workspace` ADD `is_flagged_by_anthropic` boolean;--> statement-breakpoint
ALTER TABLE `workspace` ADD `is_flagged_by_openai` boolean;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.18.3", "version": "1.18.4",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -1,4 +1,4 @@
import { json, primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { boolean, json, primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { timestamps, ulid } from "../drizzle/types" import { timestamps, ulid } from "../drizzle/types"
export const WorkspaceTable = mysqlTable( export const WorkspaceTable = mysqlTable(
@@ -8,6 +8,9 @@ export const WorkspaceTable = mysqlTable(
slug: varchar("slug", { length: 255 }), slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(), region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(),
is_blocked: boolean(),
is_flagged_by_anthropic: boolean(),
is_flagged_by_openai: boolean(),
...timestamps, ...timestamps,
}, },
(table) => [uniqueIndex("slug").on(table.slug)], (table) => [uniqueIndex("slug").on(table.slug)],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.18.3", "version": "1.18.4",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.18.3", "version": "1.18.4",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.18.3", "version": "1.18.4",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.18.3", "version": "1.18.4",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+4 -1
View File
@@ -4,7 +4,10 @@ import type { Opts, Proc } from "./pty"
export type { Disp, Exit, Opts, Proc } from "./pty" export type { Disp, Exit, Opts, Proc } from "./pty"
export function spawn(file: string, args: string[], opts: Opts): Proc { export function spawn(file: string, args: string[], opts: Opts): Proc {
const proc = pty.spawn(file, args, opts) const proc = pty.spawn(file, args, {
...opts,
...(process.platform === "win32" ? { useConptyDll: true } : {}),
})
return { return {
pid: proc.pid, pid: proc.pid,
onData(listener) { onData(listener) {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.18.3", "version": "1.18.4",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",
@@ -0,0 +1,29 @@
import { expect, test } from "bun:test"
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { wslSidecarShell } from "./sidecar-shell"
test("loads interactive WSL environment variables for the desktop server", async () => {
const home = await mkdtemp(join(tmpdir(), "opencode-wsl-env-"))
const opencode = join(home, "opencode")
await writeFile(join(home, ".profile"), 'source "$HOME/.bashrc"\n')
await writeFile(join(home, ".bashrc"), "export COMPANY_BASEURL=https://company.example\n")
await writeFile(opencode, '#!/usr/bin/env bash\nprintf "%s" "$COMPANY_BASEURL"\n')
await chmod(opencode, 0o755)
const shell = wslSidecarShell(opencode, 4096, "opencode", "secret", false)
const proc = Bun.spawn(shell.args, {
cwd: home,
env: { ...process.env, HOME: home },
stdin: new Blob([shell.script]),
stdout: "pipe",
stderr: "pipe",
})
const output = await new Response(proc.stdout).text()
await new Response(proc.stderr).text()
const code = await proc.exited
await rm(home, { recursive: true, force: true })
expect({ code, output }).toEqual({ code: 0, output: "https://company.example" })
})
@@ -0,0 +1,28 @@
import { shellEscape } from "./runtime"
export function wslSidecarShell(opencode: string, port: number, username: string, password: string, packaged: boolean) {
return {
// Explicit WSL commands skip login startup files. Load the same user environment
// as a terminal launch, then replace the interactive shell before reading stdin.
args: [
"bash",
"--noprofile",
"--norc",
"-ic",
'for file in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do [[ ! -r "$file" ]] || { source "$file"; break; }; done; exec bash -se',
],
script: [
"set -euo pipefail",
'cd "$HOME" || cd /',
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
"export PATH",
"export WSLENV=",
"export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true",
"export OPENCODE_CLIENT=desktop",
`export OPENCODE_SERVER_USERNAME=${shellEscape(username)}`,
`export OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
'export XDG_STATE_HOME="$HOME/.local/state"',
`exec ${shellEscape(opencode)} --print-logs --log-level ${packaged ? "WARN" : "INFO"} serve --hostname 0.0.0.0 --port ${port}`,
].join("\n"),
}
}
+5 -16
View File
@@ -3,7 +3,8 @@ import { randomUUID } from "node:crypto"
import { createServer } from "node:net" import { createServer } from "node:net"
import { app } from "electron" import { app } from "electron"
import { checkHealth } from "../server" import { checkHealth } from "../server"
import { type WslCommandLine, resolveWslOpencode, shellEscape, wslArgs } from "./runtime" import { type WslCommandLine, resolveWslOpencode, wslArgs } from "./runtime"
import { wslSidecarShell } from "./sidecar-shell"
import { pollWslHealth } from "./startup" import { pollWslHealth } from "./startup"
export type WslSidecar = { export type WslSidecar = {
@@ -23,24 +24,12 @@ export async function spawnWslSidecar(
const port = await allocatePort() const port = await allocatePort()
const password = randomUUID() const password = randomUUID()
const username = "opencode" const username = "opencode"
const script = [ const shell = wslSidecarShell(opencode, port, username, password, app.isPackaged)
"set -euo pipefail", const child = spawn("wsl", wslArgs(shell.args, distro), {
'cd "$HOME" || cd /',
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
"export PATH",
"export WSLENV=",
"export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true",
"export OPENCODE_CLIENT=desktop",
`export OPENCODE_SERVER_USERNAME=${shellEscape(username)}`,
`export OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
'export XDG_STATE_HOME="$HOME/.local/state"',
`exec ${shellEscape(opencode)} --print-logs --log-level ${app.isPackaged ? "WARN" : "INFO"} serve --hostname 0.0.0.0 --port ${port}`,
].join("\n")
const child = spawn("wsl", wslArgs(["bash", "-se"], distro), {
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
windowsHide: true, windowsHide: true,
}) })
child.stdin.end(script) child.stdin.end(shell.script)
const recentOutput: string[] = [] const recentOutput: string[] = []
const emit = (line: WslCommandLine) => { const emit = (line: WslCommandLine) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.18.3", "version": "1.18.4",
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.18.3", "version": "1.18.4",
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.18.3", "version": "1.18.4",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.18.3", "version": "1.18.4",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

Some files were not shown because too many files have changed in this diff Show More