Compare commits

...
Author SHA1 Message Date
Kit Langton 5eef02d8bf fix(session): omit undefined optional fields 2026-04-27 17:45:36 -04:00
Kit Langton b33579c2ec fix(httpapi): reuse exact session response schema 2026-04-27 17:29:28 -04:00
Kit Langton c3d10af784 fix(httpapi): preserve optional session fields 2026-04-27 17:18:26 -04:00
opencode-agent[bot] dfc0075f90 chore: generate 2026-04-27 20:52:42 +00:00
Kit Langton acd15dcc8a test(httpapi): cover full OpenAPI route inventory (#24667) 2026-04-27 16:51:24 -04:00
Kit Langton 139c4fd555 fix(session): harden shell cancellation (#24553) 2026-04-27 20:47:18 +00:00
Cas e0f3df8252 fix(tui): consume Enter in dialog useKeyboard handlers (#23390) 2026-04-27 15:31:49 -05:00
opencode-agent[bot] 9cd2e3a1c3 chore: generate 2026-04-27 20:31:05 +00:00
Kit Langton f584f80219 test(httpapi): verify reflected route mounts (#24663) 2026-04-27 16:29:58 -04:00
Kit Langton 45eac589f8 fix(tui): preserve Zed context on terminal focus (#24662) 2026-04-27 16:25:37 -04:00
James Long fab1768826 feat(core): file context improvements and option to disable (#24661) 2026-04-27 16:10:13 -04:00
Kit Langton 51fc10e407 fix(httpapi): enforce instance route parity (#24660) 2026-04-27 16:07:31 -04:00
opencode-agent[bot] 7a1c8465f5 chore: generate 2026-04-27 19:38:33 +00:00
Kit Langton 5290e9ca7e fix(tui): stabilize Zed editor context polling (#24656) 2026-04-27 15:37:18 -04:00
Aiden Cline c361c2953f fix: ensure toolStreaming is set to off by default when using non anthropic models with anthropic sdk (#24642) 2026-04-27 14:16:00 -05:00
opencode-agent[bot] ccb7669736 chore: generate 2026-04-27 18:34:44 +00:00
Dax f25f1485d5 refactor: remove module barrels (#24554) 2026-04-27 14:33:33 -04:00
Kit Langton 55ecb06748 fix(httpapi): accept empty session create body (#24640) 2026-04-27 17:17:11 +00:00
Kit Langton dc6991e5a8 fix(httpapi): mount workspace bridge routes (#24626) 2026-04-27 12:52:48 -04:00
Aiden Cline 738b3065dc tweak: make interleaved reasoning_content default to true for openai compat deepseek setups (#24630) 2026-04-27 10:17:38 -05:00
opencode-agent[bot] 26cc537cb1 chore: generate 2026-04-27 14:46:00 +00:00
Seashore ShiandSeashore ede354b0e6 docs: fix duplicated word in CLI env var table (#24614)
Co-authored-by: Seashore <ss@SeashoredeMac-mini.local>
2026-04-27 09:44:53 -05:00
Jack 61eabfc60c update Go DeepSeek flash limits for cache pricing drop (#24592) 2026-04-27 17:02:27 +08:00
opencode-agent[bot] 2789b770aa chore: generate 2026-04-27 05:40:37 +00:00
Luke Parker 8718b98ee1 fix: pass workspace symbol query to experimental LSP tool (#24576) 2026-04-27 05:39:36 +00:00
opencode-agent[bot] c8b2f987f9 chore: generate 2026-04-27 05:39:13 +00:00
Frank 52b55b826f Merge branch 'fix/usage-chart' into dev 2026-04-27 01:37:52 -04:00
Frank e8c20235b8 zen: coupons 2026-04-27 01:36:28 -04:00
opencode-agent[bot] 17701628bd chore: generate 2026-04-27 05:18:33 +00:00
0efc6163f1 fix(opencode): agent create generates permissions field with deny ins… (#24482)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-27 00:17:08 -05:00
Jack 1e191ba815 update Go DeepSeek request estimates for cache pricing changes (#24575) 2026-04-27 13:06:51 +08:00
Aiden Cline f19d863689 ignore: split up reasoning transforms (#24574) 2026-04-26 23:57:32 -05:00
Frank 4a1ef327ca sync 2026-04-27 00:52:54 -04:00
Aiden Cline 025a6392ce fix: default tool call streaming to false for google vertex (#24573) 2026-04-26 23:42:23 -05:00
opencode e578c442be sync release versions for v1.14.28 2026-04-27 04:23:44 +00:00
opencode-agent[bot] 0cecb1bff2 chore: generate 2026-04-27 04:05:59 +00:00
Frank 5d8971c1ed go: add deepseek icon 2026-04-27 00:04:44 -04:00
Dax a9b62d67df Refactor npm config handling (#24565) 2026-04-27 03:54:59 +00:00
Luke Parker 3525e61906 fix: ignore GitHub Actions changelog contributor (#24567) 2026-04-27 03:47:04 +00:00
github-actions[bot] 059e6c46db Update VOUCHED list
https://github.com/anomalyco/opencode/issues/24563#issuecomment-4323944984
2026-04-27 03:30:38 +00:00
Frank 5cf195e0af go: models endpoint 2026-04-26 23:02:18 -04:00
opencode 244d1debe4 sync release versions for v1.14.27 2026-04-27 02:09:07 +00:00
opencode-agent[bot] 35734b42fe chore: update nix node_modules hashes 2026-04-27 01:55:51 +00:00
400 changed files with 5325 additions and 1728 deletions
+2 -1
View File
@@ -12,8 +12,10 @@ adamdotdevin
ariane-emory ariane-emory
-atharvau AI review spamming literally every PR -atharvau AI review spamming literally every PR
-borealbytes -borealbytes
-carycooper777
-danieljoshuanazareth -danieljoshuanazareth
-danieljoshuanazareth -danieljoshuanazareth
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
edemaine edemaine
-florianleibert -florianleibert
fwang fwang
@@ -33,4 +35,3 @@ simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team -spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr thdxr
-toastythebot -toastythebot
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
+16 -16
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -83,7 +83,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -117,7 +117,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.14.26", "version": "1.14.28",
"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",
@@ -144,7 +144,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -168,7 +168,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.26", "version": "1.14.28",
"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",
@@ -192,7 +192,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.14.26", "version": "1.14.28",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -226,7 +226,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -259,7 +259,7 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -303,7 +303,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -332,7 +332,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -348,7 +348,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.14.26", "version": "1.14.28",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -491,7 +491,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
@@ -526,7 +526,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -541,7 +541,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -576,7 +576,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -625,7 +625,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.14.26", "version": "1.14.28",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
+24
View File
@@ -115,6 +115,27 @@ const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100
appliesToProducts: [zenLiteProduct.id], appliesToProducts: [zenLiteProduct.id],
duration: "once", duration: "once",
}) })
const zenLiteCouponThreeMonths100 = new stripe.Coupon("ZenLiteCoupon3Months100", {
name: "3 months 100% off",
percentOff: 100,
appliesToProducts: [zenLiteProduct.id],
duration: "repeating",
durationInMonths: 3,
})
const zenLiteCouponSixMonths100 = new stripe.Coupon("ZenLiteCoupon6Months100", {
name: "6 months 100% off",
percentOff: 100,
appliesToProducts: [zenLiteProduct.id],
duration: "repeating",
durationInMonths: 6,
})
const zenLiteCouponTwelveMonths100 = new stripe.Coupon("ZenLiteCoupon12Months100", {
name: "12 months 100% off",
percentOff: 100,
appliesToProducts: [zenLiteProduct.id],
duration: "repeating",
durationInMonths: 12,
})
const zenLitePrice = new stripe.Price("ZenLitePrice", { const zenLitePrice = new stripe.Price("ZenLitePrice", {
product: zenLiteProduct.id, product: zenLiteProduct.id,
currency: "usd", currency: "usd",
@@ -131,6 +152,9 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
priceInr: 92900, priceInr: 92900,
firstMonth50Coupon: zenLiteCouponFirstMonth50.id, firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
firstMonth100Coupon: zenLiteCouponFirstMonth100.id, firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
threeMonths100Coupon: zenLiteCouponThreeMonths100.id,
sixMonths100Coupon: zenLiteCouponSixMonths100.id,
twelveMonths100Coupon: zenLiteCouponTwelveMonths100.id,
}, },
}) })
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-zQOuZkuOLm9xV5YzfcQiWSzvussXVw/dRPw4HhuYsdc=", "x86_64-linux": "sha256-U/LZx/D+5JTT1LHSyZkEuqXP/ky7LkHrEYBW5pcVArk=",
"aarch64-linux": "sha256-J8Ak9rCCkKlBj0KhdUOklglpR3ntRP2VjtdmEJox6Ro=", "aarch64-linux": "sha256-nGZa04h4y3jbdmf87IRrlQm/E5qYR8lj5OxKgQSR2XU=",
"aarch64-darwin": "sha256-r4F6S9CfGR6qxSaSqu9AIjWCEKd/PuT1Wprcbzk0IwY=", "aarch64-darwin": "sha256-GD8pCHWMBppDaIfRKxhY2m4xWo1OrY3wOmGw+EC71mw=",
"x86_64-darwin": "sha256-I/ztb7VnLbJYzAxf2egN0+YsJcP5yix3tIoHtiuRUXE=" "x86_64-darwin": "sha256-KOH1ZB8pdpF7Xer6QIH7rrr9fwF/BZkCTJndPe0wypg="
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.26", "version": "1.14.28",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.26", "version": "1.14.28",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -204,6 +204,14 @@ export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconDeepSeek(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.249-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 0 1-.465.137 9.597 9.597 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 0 1 .415-.287.302.302 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 0 1-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" />
</svg>
)
}
export function IconMiMo(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconMiMo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+6 -3
View File
@@ -12,7 +12,7 @@ import { Footer } from "~/component/footer"
import { Header } from "~/component/header" import { Header } from "~/component/header"
import { config } from "~/config" import { config } from "~/config"
import { getLastSeenWorkspaceID } from "../workspace/common" import { getLastSeenWorkspaceID } from "../workspace/common"
import { IconMiniMax, IconMiMo, IconZai, IconAlibaba } from "~/component/icon" import { IconMiniMax, IconMiMo, IconZai, IconAlibaba, IconDeepSeek } from "~/component/icon"
import { useI18n } from "~/context/i18n" import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language" import { useLanguage } from "~/context/language"
import { LocaleLinks } from "~/component/locale-links" import { LocaleLinks } from "~/component/locale-links"
@@ -65,11 +65,11 @@ function LimitsGraph(props: { href: string }) {
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" }, { id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
{ id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" }, { id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" },
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" }, { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 1300, d: "200ms" },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" }, { id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" }, { id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7450, d: "340ms" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "200ms" },
{ id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" }, { id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
] ]
const w = 720 const w = 720
@@ -340,6 +340,9 @@ export default function Home() {
<div> <div>
<IconAlibaba width="24" height="24" /> <IconAlibaba width="24" height="24" />
</div> </div>
<div>
<IconDeepSeek width="24" height="24" />
</div>
<div> <div>
<IconMiMo width="24" height="24" /> <IconMiMo width="24" height="24" />
</div> </div>
@@ -160,8 +160,16 @@ export async function POST(input: APIEvent) {
userID: userID, userID: userID,
}) })
if (userEmail && coupon === LiteData.firstMonth100Coupon) { if (userEmail) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH") if (coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
} else if (coupon === LiteData.threeMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO3MONTHS100")
} else if (coupon === LiteData.sixMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO6MONTHS100")
} else if (coupon === LiteData.twelveMonths100Coupon) {
await Billing.redeemCoupon(userEmail, "GO12MONTHS100")
}
} }
}) })
}) })
@@ -24,15 +24,23 @@ import { useI18n } from "~/context/i18n"
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend) Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend)
async function getCosts(workspaceID: string, year: number, month: number) { async function getCosts(workspaceID: string, year: number, month: number, tzOffset: string) {
"use server" "use server"
return withActor(async () => { return withActor(async () => {
const startDate = new Date(year, month, 1) const timezoneOffset = (() => {
const endDate = new Date(year, month + 1, 1) const m = /^([+-])(\d{2}):(\d{2})$/.exec(tzOffset)
if (!m) return 0
const sign = m[1] === "-" ? -1 : 1
return sign * (Number(m[2]) * 60 + Number(m[3])) * 60_000
})()
const monthStartUTC = new Date(Date.UTC(year, month, 1, 0, 0, 0) - timezoneOffset)
const monthEndUTC = new Date(Date.UTC(year, month + 1, 1, 0, 0, 0) - timezoneOffset)
const dateExpr = sql<string>`DATE(CONVERT_TZ(${UsageTable.timeCreated}, '+00:00', ${tzOffset}))`
const usageData = await Database.use((tx) => const usageData = await Database.use((tx) =>
tx tx
.select({ .select({
date: sql<string>`DATE(${UsageTable.timeCreated})`, date: dateExpr,
model: UsageTable.model, model: UsageTable.model,
totalCost: sum(UsageTable.cost), totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID, keyId: UsageTable.keyID,
@@ -42,16 +50,11 @@ async function getCosts(workspaceID: string, year: number, month: number) {
.where( .where(
and( and(
eq(UsageTable.workspaceID, workspaceID), eq(UsageTable.workspaceID, workspaceID),
gte(UsageTable.timeCreated, startDate), gte(UsageTable.timeCreated, monthStartUTC),
lt(UsageTable.timeCreated, endDate), lt(UsageTable.timeCreated, monthEndUTC),
), ),
) )
.groupBy( .groupBy(dateExpr, UsageTable.model, UsageTable.keyID, sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`)
sql`DATE(${UsageTable.timeCreated})`,
UsageTable.model,
UsageTable.keyID,
sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
)
.then((x) => .then((x) =>
x.map((r) => ({ x.map((r) => ({
...r, ...r,
@@ -125,15 +128,45 @@ function getModelColor(model: string): string {
} }
function formatDateLabel(dateStr: string): string { function formatDateLabel(dateStr: string): string {
const date = new Date() const [, m, d] = dateStr.split("-").map(Number)
const [y, m, d] = dateStr.split("-").map(Number) const month = new Date(2000, m - 1, 1).toLocaleDateString(undefined, { month: "short" })
date.setFullYear(y) return `${month} ${d.toString().padStart(2, "0")}`
date.setMonth(m - 1) }
date.setDate(d)
date.setHours(0, 0, 0, 0) // Compute the UTC offset (in MySQL CONVERT_TZ format like "+05:30") for the
const month = date.toLocaleDateString(undefined, { month: "short" }) // given IANA timezone at the given instant. Honors DST.
const day = date.getUTCDate().toString().padStart(2, "0") function getTimezoneOffset(timezone: string, at: Date): string {
return `${month} ${day}` const parts = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
.formatToParts(at)
.reduce<Record<string, string>>((acc, p) => {
if (p.type !== "literal") acc[p.type] = p.value
return acc
}, {})
const asUTC = Date.UTC(
Number(parts.year),
Number(parts.month) - 1,
Number(parts.day),
Number(parts.hour),
Number(parts.minute),
Number(parts.second),
)
const diffMinutes = Math.round((asUTC - at.getTime()) / 60_000)
const sign = diffMinutes < 0 ? "-" : "+"
const abs = Math.abs(diffMinutes)
const hh = Math.floor(abs / 60)
.toString()
.padStart(2, "0")
const mm = (abs % 60).toString().padStart(2, "0")
return `${sign}${hh}:${mm}`
} }
function addOpacityToColor(color: string, opacity: number): string { function addOpacityToColor(color: string, opacity: number): string {
@@ -152,6 +185,7 @@ export function GraphSection() {
let chartInstance: Chart | undefined let chartInstance: Chart | undefined
const params = useParams() const params = useParams()
const i18n = useI18n() const i18n = useI18n()
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
const now = new Date() const now = new Date()
const [store, setStore] = createStore({ const [store, setStore] = createStore({
data: null as Awaited<ReturnType<typeof getCosts>> | null, data: null as Awaited<ReturnType<typeof getCosts>> | null,
@@ -185,10 +219,13 @@ export function GraphSection() {
}) })
const getDates = createMemo(() => { const getDates = createMemo(() => {
const daysInMonth = new Date(store.year, store.month + 1, 0).getDate() // Number of days in the month is independent of timezone.
const daysInMonth = new Date(Date.UTC(store.year, store.month + 1, 0)).getUTCDate()
const yyyy = store.year.toString().padStart(4, "0")
const mm = (store.month + 1).toString().padStart(2, "0")
return Array.from({ length: daysInMonth }, (_, i) => { return Array.from({ length: daysInMonth }, (_, i) => {
const date = new Date(store.year, store.month, i + 1) const dd = (i + 1).toString().padStart(2, "0")
return date.toISOString().split("T")[0] return `${yyyy}-${mm}-${dd}`
}) })
}) })
@@ -415,7 +452,11 @@ export function GraphSection() {
}) })
createEffect(async () => { createEffect(async () => {
const data = await getCosts(params.id!, store.year, store.month) // Compute the offset for mid-month so DST transitions don't bias to the
// wrong side.
const midMonth = new Date(Date.UTC(store.year, store.month, 15, 12, 0, 0))
const tzOffset = getTimezoneOffset(timezone, midMonth)
const data = await getCosts(params.id!, store.year, store.month, tzOffset)
setStore({ data }) setStore({ data })
}) })
@@ -1,10 +1,12 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { getHandler, optionsHandler } from "../../util/modelsHandler" import { ZenData } from "@opencode-ai/console-core/model.js"
import { buildModelsResponse, buildOptionsResponse } from "../../util/modelsHandler"
export async function OPTIONS(_input: APIEvent) { export async function OPTIONS(_input: APIEvent) {
return optionsHandler() return buildOptionsResponse()
} }
export async function GET(input: APIEvent) { export async function GET(_input: APIEvent) {
return getHandler({ modelList: "lite" }) const models = Object.keys(ZenData.list("lite").models)
return buildModelsResponse(models)
} }
@@ -1,6 +1,4 @@
import { ZenData } from "@opencode-ai/console-core/model.js" export async function buildOptionsResponse() {
export async function optionsHandler() {
return new Response(null, { return new Response(null, {
status: 200, status: 200,
headers: { headers: {
@@ -11,16 +9,13 @@ export async function optionsHandler() {
}) })
} }
export async function getHandler(opts: { modelList: "lite" | "full"; disabledModels?: string[] }) { export async function buildModelsResponse(models: string[]) {
const zenData = ZenData.list(opts.modelList)
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
object: "list", object: "list",
data: Object.entries(zenData.models) data: models
.filter(([id]) => !opts.disabledModels?.includes(id)) .filter((id) => !id.startsWith("alpha-"))
.filter(([id]) => !id.startsWith("alpha-")) .map((id) => ({
.map(([id, _model]) => ({
id, id,
object: "model", object: "model",
created: Math.floor(Date.now() / 1000), created: Math.floor(Date.now() / 1000),
@@ -1,18 +1,19 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { optionsHandler, getHandler } from "~/routes/zen/util/modelsHandler" import { buildOptionsResponse, buildModelsResponse } from "~/routes/zen/util/modelsHandler"
export async function OPTIONS(_input: APIEvent) { export async function OPTIONS(_input: APIEvent) {
return optionsHandler() return buildOptionsResponse()
} }
export async function GET(input: APIEvent) { export async function GET(input: APIEvent) {
const disabledModels = await (() => { const disabledModels = await (() => {
const apiKey = input.request.headers.get("authorization")?.split(" ")[1] const apiKey = input.request.headers.get("authorization")?.split(" ")[1]
if (!apiKey) return [] if (!apiKey) return [] as string[]
return Database.use((tx) => return Database.use((tx) =>
tx tx
@@ -27,5 +28,7 @@ export async function GET(input: APIEvent) {
) )
})() })()
return getHandler({ modelList: "full", disabledModels }) const models = Object.keys(ZenData.list("full").models).filter((id) => !disabledModels.includes(id))
return buildModelsResponse(models)
} }
@@ -0,0 +1 @@
ALTER TABLE `coupon` MODIFY COLUMN `type` enum('BUILDATHON','GOFREEMONTH','GO3MONTHS100','GO6MONTHS100','GO12MONTHS100') NOT NULL;
+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.14.26", "version": "1.14.28",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+14 -7
View File
@@ -176,13 +176,13 @@ export namespace Billing {
) )
} }
export const hasCoupon = async (email: string, type: (typeof CouponType)[number]) => { export const getCoupons = async (email: string) => {
return await Database.use((tx) => return await Database.use((tx) =>
tx tx
.select() .select({ type: CouponTable.type, timeRedeemed: CouponTable.timeRedeemed })
.from(CouponTable) .from(CouponTable)
.where(and(eq(CouponTable.email, email), eq(CouponTable.type, type), isNull(CouponTable.timeRedeemed))) .where(and(eq(CouponTable.email, email), isNull(CouponTable.timeRedeemed)))
.then((rows) => rows.length > 0), .then((rows) => rows.map((row) => row.type)),
) )
} }
@@ -290,9 +290,16 @@ export namespace Billing {
if (billing.subscriptionID) throw new Error("Already subscribed to Black") if (billing.subscriptionID) throw new Error("Already subscribed to Black")
if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite") if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite")
const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH")) const coupons = await Billing.getCoupons(email)
? LiteData.firstMonth100Coupon const coupon = coupons.includes("GO12MONTHS100")
: LiteData.firstMonth50Coupon ? LiteData.twelveMonths100Coupon
: coupons.includes("GO6MONTHS100")
? LiteData.sixMonths100Coupon
: coupons.includes("GO3MONTHS100")
? LiteData.threeMonths100Coupon
: coupons.includes("GOFREEMONTH")
? LiteData.firstMonth100Coupon
: LiteData.firstMonth50Coupon
const createSession = () => const createSession = () =>
Billing.stripe().checkout.sessions.create({ Billing.stripe().checkout.sessions.create({
mode: "subscription", mode: "subscription",
+3
View File
@@ -13,5 +13,8 @@ export namespace LiteData {
export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr) export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
export const firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon export const firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon
export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon
export const threeMonths100Coupon = Resource.ZEN_LITE_PRICE.threeMonths100Coupon
export const sixMonths100Coupon = Resource.ZEN_LITE_PRICE.sixMonths100Coupon
export const twelveMonths100Coupon = Resource.ZEN_LITE_PRICE.twelveMonths100Coupon
export const planName = fn(z.void(), () => "lite") export const planName = fn(z.void(), () => "lite")
} }
@@ -133,7 +133,7 @@ export const UsageTable = mysqlTable(
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)], (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
) )
export const CouponType = ["BUILDATHON", "GOFREEMONTH"] as const export const CouponType = ["BUILDATHON", "GOFREEMONTH", "GO3MONTHS100", "GO6MONTHS100", "GO12MONTHS100"] as const
export const CouponTable = mysqlTable( export const CouponTable = mysqlTable(
"coupon", "coupon",
{ {
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string "price": string
"priceInr": number "priceInr": number
"product": string "product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable" "type": "sst.sst.Linkable"
} }
"ZEN_MODELS1": { "ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.26", "version": "1.14.28",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string "price": string
"priceInr": number "priceInr": number
"product": string "product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable" "type": "sst.sst.Linkable"
} }
"ZEN_MODELS1": { "ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.26", "version": "1.14.28",
"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",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string "price": string
"priceInr": number "priceInr": number
"product": string "product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable" "type": "sst.sst.Linkable"
} }
"ZEN_MODELS1": { "ZEN_MODELS1": {
+2 -1
View File
@@ -1,12 +1,13 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.26", "version": "1.14.28",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"test": "bun test", "test": "bun test",
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
"typecheck": "tsgo --noEmit" "typecheck": "tsgo --noEmit"
}, },
"bin": { "bin": {
+40
View File
@@ -0,0 +1,40 @@
export * as NpmConfig from "./npm-config"
import { fileURLToPath } from "url"
// @ts-expect-error npm does not publish types for this internal config API.
import Config from "@npmcli/config"
// @ts-expect-error npm does not publish types for this internal config API.
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
import { Effect } from "effect"
const npmPath = fileURLToPath(new URL("..", import.meta.url))
export const load = (dir: string) =>
Effect.tryPromise({
try: async () => {
const config = new Config({
npmPath,
cwd: dir,
env: { ...process.env },
argv: [process.execPath, process.execPath],
execPath: process.execPath,
platform: process.platform,
definitions,
flatten,
nerfDarts,
shorthands,
warn: false,
})
await config.load()
return config.flat as Record<string, unknown>
},
catch: (cause) => cause,
}).pipe(Effect.orElseSucceed(() => ({}) as Record<string, unknown>))
export const registry = (dir: string) =>
load(dir).pipe(
Effect.map((config) => {
const registry = typeof config.registry === "string" ? config.registry : "https://registry.npmjs.org"
return registry.endsWith("/") ? registry.slice(0, -1) : registry
}),
)
+3 -83
View File
@@ -1,22 +1,14 @@
export * as Npm from "./npm" export * as Npm from "./npm"
import path from "path" import path from "path"
import { fileURLToPath } from "url"
import npa from "npm-package-arg" import npa from "npm-package-arg"
import semver from "semver" import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
// @ts-expect-error npm does not publish types for this internal config API.
import Config from "@npmcli/config"
// @ts-expect-error npm does not publish types for this internal config API.
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect"
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "./filesystem" import { AppFileSystem } from "./filesystem"
import { Global } from "./global" import { Global } from "./global"
import { EffectFlock } from "./util/effect-flock" import { EffectFlock } from "./util/effect-flock"
import { makeRuntime } from "./effect/runtime" import { makeRuntime } from "./effect/runtime"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NpmConfig } from "./npm-config"
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", { export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional), add: Schema.Array(Schema.String).pipe(Schema.optional),
@@ -40,46 +32,18 @@ export interface Interface {
}[] }[]
}, },
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError> ) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect<boolean>
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>> readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
const npmPath = fileURLToPath(new URL("..", import.meta.url))
export function sanitize(pkg: string) { export function sanitize(pkg: string) {
if (!illegal) return pkg if (!illegal) return pkg
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
} }
const loadOptions = (dir: string) =>
Effect.tryPromise({
try: async () => {
const config = new Config({
npmPath,
cwd: dir,
env: { ...process.env },
argv: [process.execPath, process.execPath],
execPath: process.execPath,
platform: process.platform,
definitions,
flatten,
nerfDarts,
shorthands,
warn: false,
})
await config.load()
return config.flat
},
catch: (cause) =>
new InstallFailedError({
cause,
dir,
}),
})
const resolveEntryPoint = (name: string, dir: string): EntryPoint => { const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string> let entrypoint: Option.Option<string>
try { try {
@@ -110,39 +74,13 @@ export const layer = Layer.effect(
const global = yield* Global.Service const global = yield* Global.Service
const fs = yield* FileSystem.FileSystem const fs = yield* FileSystem.FileSystem
const flock = yield* EffectFlock.Service const flock = yield* EffectFlock.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
const runView = Effect.fnUntraced(function* (cmd: string[]) {
const handle = yield* spawner.spawn(
ChildProcess.make(cmd[0], cmd.slice(1), {
extendEnv: true,
}),
)
const [stdout, stderr] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
{ concurrency: 2 },
)
const code = yield* handle.exitCode
if (code !== 0 || !stdout.trim()) {
return yield* Effect.fail(stderr || stdout || `Failed to run ${cmd.join(" ")}`)
}
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String))(stdout)
}, Effect.scoped)
const viewLatestVersion = Effect.fnUntraced(function* (pkg: string) {
return yield* runView(["npm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
Effect.catch(() =>
runView(["pnpm", "view", pkg, "dist-tags.latest", "--json"]).pipe(
Effect.catch(() => runView(["bun", "pm", "view", pkg, "dist-tags.latest", "--json"])),
),
),
)
})
const reify = (input: { dir: string; add?: string[] }) => const reify = (input: { dir: string; add?: string[] }) =>
Effect.gen(function* () { Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`) yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const add = input.add ?? [] const add = input.add ?? []
const npmOptions = yield* loadOptions(input.dir) const npmOptions = yield* NpmConfig.load(input.dir)
const arborist = new Arborist({ const arborist = new Arborist({
...npmOptions, ...npmOptions,
path: input.dir, path: input.dir,
@@ -172,18 +110,6 @@ export const layer = Layer.effect(
}), }),
) )
const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) {
const latestVersion = yield* viewLatestVersion(pkg).pipe(Effect.option)
if (Option.isNone(latestVersion)) {
return false
}
const range = /[\s^~*xX<>|=]/.test(cachedVersion)
if (range) return !semver.satisfies(latestVersion.value, cachedVersion)
return semver.lt(cachedVersion, latestVersion.value)
})
const add = Effect.fn("Npm.add")(function* (pkg: string) { const add = Effect.fn("Npm.add")(function* (pkg: string) {
const dir = directory(pkg) const dir = directory(pkg)
const name = (() => { const name = (() => {
@@ -309,7 +235,6 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
add, add,
install, install,
outdated,
which, which,
}) })
}), }),
@@ -320,7 +245,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(AppFileSystem.layer), Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer), Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer), Layer.provide(NodeFileSystem.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
@@ -337,10 +261,6 @@ export async function add(...args: Parameters<Interface["add"]>) {
} }
} }
export async function outdated(...args: Parameters<Interface["outdated"]>) {
return runPromise((svc) => svc.outdated(...args))
}
export async function which(...args: Parameters<Interface["which"]>) { export async function which(...args: Parameters<Interface["which"]>) {
const resolved = await runPromise((svc) => svc.which(...args)) const resolved = await runPromise((svc) => svc.which(...args))
return Option.getOrUndefined(resolved) return Option.getOrUndefined(resolved)
+13
View File
@@ -0,0 +1,13 @@
import fs from "fs/promises"
import { tmpdir as osTmpdir } from "os"
import path from "path"
export const tmpdir = async () => {
const dir = await fs.mkdtemp(path.join(osTmpdir(), "opencode-core-test-"))
return {
path: dir,
async [Symbol.asyncDispose]() {
await fs.rm(dir, { recursive: true, force: true })
},
}
}
+51
View File
@@ -0,0 +1,51 @@
import path from "path"
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { NpmConfig } from "@opencode-ai/core/npm-config"
import { tmpdir } from "./fixture/tmpdir"
describe("NpmConfig.load", () => {
test("reads registry from project .npmrc", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n")
const config = await Effect.runPromise(NpmConfig.load(tmp.path))
expect(config.registry).toBe("https://registry.example.test/")
})
test("reads scoped registries from project .npmrc", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, ".npmrc"), "@acme:registry=https://npm.acme.test/\n")
const config = await Effect.runPromise(NpmConfig.load(tmp.path))
expect(config["@acme:registry"]).toBe("https://npm.acme.test/")
})
test("flattens boolean and list options", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, ".npmrc"), "ignore-scripts=true\nomit[]=dev\nomit[]=optional\n")
const config = await Effect.runPromise(NpmConfig.load(tmp.path))
expect(config.ignoreScripts).toBe(true)
expect(config.omit).toEqual(["dev", "optional"])
})
})
describe("NpmConfig.registry", () => {
test("normalizes configured registry without trailing slash", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n")
await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test")
})
test("leaves configured registry without trailing slash unchanged", async () => {
await using tmp = await tmpdir()
await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test\n")
await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test")
})
})
+56
View File
@@ -0,0 +1,56 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect, test } from "bun:test"
import { Npm } from "@opencode-ai/core/npm"
import { tmpdir } from "./fixture/tmpdir"
const win = process.platform === "win32"
const writePackage = (dir: string, pkg: Record<string, unknown>) =>
Bun.write(
path.join(dir, "package.json"),
JSON.stringify({
version: "1.0.0",
...pkg,
}),
)
describe("Npm.sanitize", () => {
test("keeps normal scoped package specs unchanged", () => {
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0")
expect(Npm.sanitize("prettier")).toBe("prettier")
})
test("handles git https specs", () => {
const spec = "acme@git+https://github.com/opencode/acme.git"
const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec
expect(Npm.sanitize(spec)).toBe(expected)
})
})
describe("Npm.install", () => {
test("respects omit from project .npmrc", async () => {
await using tmp = await tmpdir()
await writePackage(tmp.path, {
name: "fixture",
dependencies: {
"prod-pkg": "file:./prod-pkg",
},
devDependencies: {
"dev-pkg": "file:./dev-pkg",
},
})
await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")
await fs.mkdir(path.join(tmp.path, "prod-pkg"))
await fs.mkdir(path.join(tmp.path, "dev-pkg"))
await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" })
await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" })
await Npm.install(tmp.path)
await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined()
await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow()
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"private": true, "private": true,
"version": "1.14.26", "version": "1.14.28",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.14.26", "version": "1.14.28",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.14.26", "version": "1.14.28",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string "price": string
"priceInr": number "priceInr": number
"product": string "product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable" "type": "sst.sst.Linkable"
} }
"ZEN_MODELS1": { "ZEN_MODELS1": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.14.26" version = "1.14.28"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.26/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.26/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.26/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.26/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.26/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.14.26", "version": "1.14.28",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string "price": string
"priceInr": number "priceInr": number
"product": string "product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable" "type": "sst.sst.Linkable"
} }
"ZEN_MODELS1": { "ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.26", "version": "1.14.28",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { z } from "zod" import { z } from "zod"
import { Config } from "../src/config" import { Config } from "@/config/config"
import { TuiConfig } from "../src/cli/cmd/tui/config/tui" import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
function generate(schema: z.ZodType) { function generate(schema: z.ZodType) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage" import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema" import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url" import { normalizeServerUrl } from "./url"
+4 -4
View File
@@ -31,19 +31,19 @@ import {
type Usage, type Usage,
} from "@agentclientprotocol/sdk" } from "@agentclientprotocol/sdk"
import { Log } from "../util" import * as Log from "@opencode-ai/core/util/log"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { Filesystem } from "../util" import { Filesystem } from "@/util/filesystem"
import { Hash } from "@opencode-ai/core/util/hash" import { Hash } from "@opencode-ai/core/util/hash"
import { ACPSessionManager } from "./session" import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types" import type { ACPConfig } from "./types"
import { Provider } from "../provider" import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { Agent as AgentModule } from "../agent/agent" import { Agent as AgentModule } from "../agent/agent"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2" import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config" import { Config } from "@/config/config"
import { ConfigMCP } from "@/config/mcp" import { ConfigMCP } from "@/config/mcp"
import { Todo } from "@/session/todo" import { Todo } from "@/session/todo"
import { Result, Schema } from "effect" import { Result, Schema } from "effect"
+1 -1
View File
@@ -1,6 +1,6 @@
import { RequestError, type McpServer } from "@agentclientprotocol/sdk" import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
import type { ACPSessionState } from "./types" import type { ACPSessionState } from "./types"
import { Log } from "@/util" import * as Log from "@opencode-ai/core/util/log"
import type { OpencodeClient } from "@opencode-ai/sdk/v2" import type { OpencodeClient } from "@opencode-ai/sdk/v2"
const log = Log.create({ service: "acp-session-manager" }) const log = Log.create({ service: "acp-session-manager" })
+5 -5
View File
@@ -1,11 +1,11 @@
import { Config } from "../config" import { Config } from "@/config/config"
import z from "zod" import z from "zod"
import { Provider } from "../provider" import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai" import { generateObject, streamObject, type ModelMessage } from "ai"
import { Truncate } from "../tool" import { Truncate } from "@/tool/truncate"
import { Auth } from "../auth" import { Auth } from "../auth"
import { ProviderTransform } from "../provider" import { ProviderTransform } from "@/provider/transform"
import PROMPT_GENERATE from "./generate.txt" import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt"
@@ -19,7 +19,7 @@ import path from "path"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Effect, Context, Layer, Schema } from "effect" import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect/instance-state"
import * as Option from "effect/Option" import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer" import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
+3 -3
View File
@@ -1,9 +1,9 @@
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
import { EffectBridge } from "@/effect" import { EffectBridge } from "@/effect/bridge"
import { Log } from "../util" import * as Log from "@opencode-ai/core/util/log"
import { BusEvent } from "./bus-event" import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global" import { GlobalBus } from "./global"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
const log = Log.create({ service: "bus" }) const log = Log.create({ service: "bus" })
+1 -1
View File
@@ -1,4 +1,4 @@
import { Log } from "@/util" import * as Log from "@opencode-ai/core/util/log"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
+42 -25
View File
@@ -4,10 +4,10 @@ import { AppRuntime } from "@/effect/app-runtime"
import { UI } from "../ui" import { UI } from "../ui"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Agent } from "../../agent/agent" import { Agent } from "../../agent/agent"
import { Provider } from "../../provider" import { Provider } from "@/provider/provider"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import matter from "gray-matter" import matter from "gray-matter"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { EOL } from "os" import { EOL } from "os"
@@ -15,7 +15,23 @@ import type { Argv } from "yargs"
type AgentMode = "all" | "primary" | "subagent" type AgentMode = "all" | "primary" | "subagent"
const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "glob", "grep", "webfetch", "task", "todowrite"] // Permission keys (not raw tool names). Multiple tools can map to a single
// permission — e.g. write/edit/apply_patch all gate on `edit` — so we configure
// agents at the permission level to match how the runtime actually enforces it.
const AVAILABLE_PERMISSIONS = [
"bash",
"read",
"edit",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
"websearch",
"codesearch",
"lsp",
"skill",
]
const AgentCreateCommand = cmd({ const AgentCreateCommand = cmd({
command: "create", command: "create",
@@ -35,9 +51,10 @@ const AgentCreateCommand = cmd({
describe: "agent mode", describe: "agent mode",
choices: ["all", "primary", "subagent"] as const, choices: ["all", "primary", "subagent"] as const,
}) })
.option("tools", { .option("permissions", {
type: "string", type: "string",
describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`, alias: ["tools"],
describe: `comma-separated list of permissions to allow (default: all). Available: "${AVAILABLE_PERMISSIONS.join(", ")}"`,
}) })
.option("model", { .option("model", {
type: "string", type: "string",
@@ -51,9 +68,9 @@ const AgentCreateCommand = cmd({
const cliPath = args.path const cliPath = args.path
const cliDescription = args.description const cliDescription = args.description
const cliMode = args.mode as AgentMode | undefined const cliMode = args.mode as AgentMode | undefined
const cliTools = args.tools const perms = args.permissions
const isFullyNonInteractive = cliPath && cliDescription && cliMode && cliTools !== undefined const isFullyNonInteractive = cliPath && cliDescription && cliMode && perms !== undefined
if (!isFullyNonInteractive) { if (!isFullyNonInteractive) {
UI.empty() UI.empty()
@@ -120,21 +137,21 @@ const AgentCreateCommand = cmd({
}) })
spinner.stop(`Agent ${generated.identifier} generated`) spinner.stop(`Agent ${generated.identifier} generated`)
// Select tools // Select permissions to allow
let selectedTools: string[] let selected: string[]
if (cliTools !== undefined) { if (perms !== undefined) {
selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS selected = perms ? perms.split(",").map((t) => t.trim()) : AVAILABLE_PERMISSIONS
} else { } else {
const result = await prompts.multiselect({ const result = await prompts.multiselect({
message: "Select tools to enable (Space to toggle)", message: "Select permissions to allow (Space to toggle)",
options: AVAILABLE_TOOLS.map((tool) => ({ options: AVAILABLE_PERMISSIONS.map((permission) => ({
label: tool, label: permission,
value: tool, value: permission,
})), })),
initialValues: AVAILABLE_TOOLS, initialValues: AVAILABLE_PERMISSIONS,
}) })
if (prompts.isCancel(result)) throw new UI.CancelledError() if (prompts.isCancel(result)) throw new UI.CancelledError()
selectedTools = result selected = result
} }
// Get mode // Get mode
@@ -167,11 +184,11 @@ const AgentCreateCommand = cmd({
mode = modeResult mode = modeResult
} }
// Build tools config // Build permissions config — deny anything not explicitly selected.
const tools: Record<string, boolean> = {} const permissions: Record<string, "deny"> = {}
for (const tool of AVAILABLE_TOOLS) { for (const permission of AVAILABLE_PERMISSIONS) {
if (!selectedTools.includes(tool)) { if (!selected.includes(permission)) {
tools[tool] = false permissions[permission] = "deny"
} }
} }
@@ -179,13 +196,13 @@ const AgentCreateCommand = cmd({
const frontmatter: { const frontmatter: {
description: string description: string
mode: AgentMode mode: AgentMode
tools?: Record<string, boolean> permission?: Record<string, "deny">
} = { } = {
description: generated.whenToUse, description: generated.whenToUse,
mode, mode,
} }
if (Object.keys(tools).length > 0) { if (Object.keys(permissions).length > 0) {
frontmatter.tools = tools frontmatter.permission = permissions
} }
// Write file // Write file
+2 -2
View File
@@ -1,11 +1,11 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { spawn } from "child_process" import { spawn } from "child_process"
import { Database } from "../../storage" import { Database } from "@/storage/db"
import { drizzle } from "drizzle-orm/bun-sqlite" import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite" import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { JsonMigration } from "../../storage" import { JsonMigration } from "@/storage/json-migration"
import { EOL } from "os" import { EOL } from "os"
import { errorMessage } from "../../util/error" import { errorMessage } from "../../util/error"
+3 -3
View File
@@ -2,11 +2,11 @@ import { EOL } from "os"
import { basename } from "path" import { basename } from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Agent } from "../../../agent/agent" import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider" import { Provider } from "@/provider/provider"
import { Session } from "../../../session" import { Session } from "@/session/session"
import type { MessageV2 } from "../../../session/message-v2" import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema" import { MessageID, PartID } from "../../../session/schema"
import { ToolRegistry } from "../../../tool" import { ToolRegistry } from "@/tool/registry"
import { Instance } from "../../../project/instance" import { Instance } from "../../../project/instance"
import { Permission } from "../../../permission" import { Permission } from "../../../permission"
import { iife } from "../../../util/iife" import { iife } from "../../../util/iife"
@@ -1,5 +1,5 @@
import { EOL } from "os" import { EOL } from "os"
import { Config } from "../../../config" import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
+2 -2
View File
@@ -1,9 +1,9 @@
import { LSP } from "../../../lsp" import { LSP } from "@/lsp/lsp"
import { AppRuntime } from "../../../effect/app-runtime" import { AppRuntime } from "../../../effect/app-runtime"
import { Effect } from "effect" import { Effect } from "effect"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { Log } from "../../../util" import * as Log from "@opencode-ai/core/util/log"
import { EOL } from "os" import { EOL } from "os"
export const LSPCommand = cmd({ export const LSPCommand = cmd({
+2 -2
View File
@@ -1,6 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { Project } from "../../../project" import { Project } from "@/project/project"
import { Log } from "../../../util" import * as Log from "@opencode-ai/core/util/log"
import { cmd } from "../cmd" import { cmd } from "../cmd"
export const ScrapCommand = cmd({ export const ScrapCommand = cmd({
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { Session } from "../../session" import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema" import { SessionID } from "../../session/schema"
import { cmd } from "./cmd" import { cmd } from "./cmd"
+6 -6
View File
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { exec } from "child_process" import { exec } from "child_process"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda" import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest" import { Octokit } from "@octokit/rest"
@@ -18,21 +18,21 @@ import type {
} from "@octokit/webhooks-types" } from "@octokit/webhooks-types"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { ModelsDev } from "../../provider" import { ModelsDev } from "@/provider/models"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share" import { SessionShare } from "@/share/session"
import { Session } from "../../session" import { Session } from "@/session/session"
import type { SessionID } from "../../session/schema" import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema" import { MessageID, PartID } from "../../session/schema"
import { Provider } from "../../provider" import { Provider } from "@/provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt" import { SessionPrompt } from "@/session/prompt"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util" import { Process } from "@/util/process"
import { Effect } from "effect" import { Effect } from "effect"
type GitHubAuthor = { type GitHubAuthor = {
+4 -4
View File
@@ -1,15 +1,15 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2" import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { Session } from "../../session" import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage" import { Database } from "@/storage/db"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { ShareNext } from "../../share" import { ShareNext } from "@/share/share-next"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Schema } from "effect" import { Schema } from "effect"
+2 -2
View File
@@ -7,7 +7,7 @@ import { UI } from "../ui"
import { MCP } from "../../mcp" import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth" import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider" import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "../../config" import { Config } from "@/config/config"
import { ConfigMCP } from "../../config/mcp" import { ConfigMCP } from "../../config/mcp"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Installation } from "../../installation" import { Installation } from "../../installation"
@@ -15,7 +15,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path" import path from "path"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { modify, applyEdits } from "jsonc-parser" import { modify, applyEdits } from "jsonc-parser"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { AppRuntime } from "../../effect/app-runtime" import { AppRuntime } from "../../effect/app-runtime"
import { Effect } from "effect" import { Effect } from "effect"
+2 -2
View File
@@ -1,8 +1,8 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Provider } from "../../provider" import { Provider } from "@/provider/provider"
import { ProviderID } from "../../provider/schema" import { ProviderID } from "../../provider/schema"
import { ModelsDev } from "../../provider" import { ModelsDev } from "@/provider/models"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { UI } from "../ui" import { UI } from "../ui"
import { EOL } from "os" import { EOL } from "os"
+3 -3
View File
@@ -1,14 +1,14 @@
import { intro, log, outro, spinner } from "@clack/prompts" import { intro, log, outro, spinner } from "@clack/prompts"
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { ConfigPaths } from "../../config" import { ConfigPaths } from "@/config/paths"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
import { resolvePluginTarget } from "../../plugin/shared" import { resolvePluginTarget } from "../../plugin/shared"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { errorMessage } from "../../util/error" import { errorMessage } from "../../util/error"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import { Process } from "../../util" import { Process } from "@/util/process"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
+1 -1
View File
@@ -3,7 +3,7 @@ import { cmd } from "./cmd"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Process } from "@/util" import { Process } from "@/util/process"
export const PrCommand = cmd({ export const PrCommand = cmd({
command: "pr <number>", command: "pr <number>",
+3 -3
View File
@@ -3,16 +3,16 @@ import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { UI } from "../ui" import { UI } from "../ui"
import { ModelsDev } from "../../provider" import { ModelsDev } from "@/provider/models"
import { map, pipe, sortBy, values } from "remeda" import { map, pipe, sortBy, values } from "remeda"
import path from "path" import path from "path"
import os from "os" import os from "os"
import { Config } from "../../config" import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Plugin } from "../../plugin" import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin" import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util" import { Process } from "@/util/process"
import { text } from "node:stream/consumers" import { text } from "node:stream/consumers"
import { Effect } from "effect" import { Effect } from "effect"
+4 -4
View File
@@ -6,13 +6,13 @@ import { cmd } from "./cmd"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server" import { Server } from "../../server/server"
import { Provider } from "../../provider" import { Provider } from "@/provider/provider"
import { Agent } from "../../agent/agent" import { Agent } from "../../agent/agent"
import { Permission } from "../../permission" import { Permission } from "../../permission"
import { Tool } from "../../tool" import { Tool } from "@/tool/tool"
import { GlobTool } from "../../tool/glob" import { GlobTool } from "../../tool/glob"
import { GrepTool } from "../../tool/grep" import { GrepTool } from "../../tool/grep"
import { ReadTool } from "../../tool/read" import { ReadTool } from "../../tool/read"
@@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task"
import { SkillTool } from "../../tool/skill" import { SkillTool } from "../../tool/skill"
import { BashTool } from "../../tool/bash" import { BashTool } from "../../tool/bash"
import { TodoWriteTool } from "../../tool/todo" import { TodoWriteTool } from "../../tool/todo"
import { Locale } from "../../util" import { Locale } from "@/util/locale"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
type ToolProps<T> = { type ToolProps<T> = {
+4 -4
View File
@@ -1,13 +1,13 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Session } from "../../session" import { Session } from "@/session/session"
import { SessionID } from "../../session/schema" import { SessionID } from "../../session/schema"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { UI } from "../ui" import { UI } from "../ui"
import { Locale } from "../../util" import { Locale } from "@/util/locale"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Filesystem } from "../../util" import { Filesystem } from "@/util/filesystem"
import { Process } from "../../util" import { Process } from "@/util/process"
import { EOL } from "os" import { EOL } from "os"
import path from "path" import path from "path"
import { which } from "../../util/which" import { which } from "../../util/which"
+3 -3
View File
@@ -1,10 +1,10 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Session } from "../../session" import { Session } from "@/session/session"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage" import { Database } from "@/storage/db"
import { SessionTable } from "../../session/session.sql" import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project" import { Project } from "@/project/project"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
+14 -3
View File
@@ -50,16 +50,18 @@ import { DialogAlert } from "./ui/dialog-alert"
import { DialogConfirm } from "./ui/dialog-confirm" import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast" import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit" import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session" import { Session as SessionApi } from "@/session/session"
import { TuiEvent } from "./event" import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv" import { KVProvider, useKV } from "./context/kv"
import { Provider } from "@/provider" import { Provider } from "@/provider/provider"
import { ArgsProvider, useArgs, type Args } from "./context/args" import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open" import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt" import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
@@ -725,6 +727,15 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
dialog.clear() dialog.clear()
}, },
}, },
{
title: kv.get("file_context_enabled", true) ? "Disable file context" : "Enable file context",
value: "app.toggle.file_context",
category: "System",
onSelect: (dialog) => {
kv.set("file_context_enabled", !kv.get("file_context_enabled", true))
dialog.clear()
},
},
{ {
title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping", title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
value: "app.toggle.diffwrap", value: "app.toggle.diffwrap",
@@ -77,6 +77,8 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
return return
} }
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
if (selected() === "subscribe") subscribe(props, dialog) if (selected() === "subscribe") subscribe(props, dialog)
else dismiss(props, dialog) else dismiss(props, dialog)
} }
@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
import { map, pipe, entries, sortBy } from "remeda" import { map, pipe, entries, sortBy } from "remeda"
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
@@ -42,6 +42,8 @@ export function DialogSessionDeleteFailed(props: {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
void confirm() void confirm()
} }
if (evt.name === "left" || evt.name === "up") { if (evt.name === "left" || evt.name === "up") {
@@ -3,14 +3,14 @@ import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { createMemo, createResource, createSignal, onMount } from "solid-js" import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { useProject } from "@tui/context/project" import { useProject } from "@tui/context/project"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk" import { useSDK } from "../context/sdk"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename" import { DialogSessionRename } from "./dialog-session-rename"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import { createDebouncedSignal } from "../util/signal" import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create" import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
@@ -1,7 +1,7 @@
import { useDialog } from "@tui/ui/dialog" import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select" import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createSignal } from "solid-js" import { createMemo, createSignal } from "solid-js"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { usePromptStash, type StashEntry } from "./prompt/stash" import { usePromptStash, type StashEntry } from "./prompt/stash"
@@ -14,7 +14,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { useCommandDialog } from "@tui/component/dialog-command" import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid" import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history" import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency" import { useFrecency } from "./frecency"
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store" import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -3,7 +3,7 @@ import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, S
import "opentui-spinner/solid" import "opentui-spinner/solid"
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { tint, useTheme } from "@tui/context/theme" import { tint, useTheme } from "@tui/context/theme"
import { EmptyBorder, SplitBorder } from "@tui/component/border" import { EmptyBorder, SplitBorder } from "@tui/component/border"
@@ -29,7 +29,7 @@ import * as Clipboard from "../../util/clipboard"
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event" import { TuiEvent } from "../../event"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { formatDuration } from "@/util/format" import { formatDuration } from "@/util/format"
import { createColors, createFrames } from "../../ui/spinner.ts" import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog" import { useDialog } from "@tui/ui/dialog"
@@ -112,9 +112,10 @@ export function Prompt(props: PromptProps) {
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true)) const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
const list = createMemo(() => props.placeholders?.normal ?? []) const list = createMemo(() => props.placeholders?.normal ?? [])
const shell = createMemo(() => props.placeholders?.shell ?? []) const shell = createMemo(() => props.placeholders?.shell ?? [])
const editorPath = createMemo(() => editor.selection()?.filePath) const fileContextEnabled = createMemo(() => kv.get("file_context_enabled", true))
const editorPath = createMemo(() => (fileContextEnabled() ? editor.selection()?.filePath : undefined))
const editorSelectionLabel = createMemo(() => { const editorSelectionLabel = createMemo(() => {
const selection = editor.selection()?.selection const selection = fileContextEnabled() ? editor.selection()?.selection : undefined
if (!selection) return if (!selection) return
if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return if (selection.start.line === selection.end.line && selection.start.character === selection.end.character) return
if (selection.start.line === selection.end.line) return `#${selection.start.line}` if (selection.start.line === selection.end.line) return `#${selection.start.line}`
@@ -746,7 +747,7 @@ export function Prompt(props: PromptProps) {
// Capture mode before it gets reset // Capture mode before it gets reset
const currentMode = store.mode const currentMode = store.mode
const variant = local.model.variant.current() const variant = local.model.variant.current()
const editorSelection = editor.selection() const editorSelection = fileContextEnabled() ? editor.selection() : undefined
const editorParts = editorSelection const editorParts = editorSelection
? [ ? [
{ {
@@ -755,15 +756,25 @@ export function Prompt(props: PromptProps) {
text: (() => { text: (() => {
const start = editorSelection.selection.start const start = editorSelection.selection.start
const end = editorSelection.selection.end const end = editorSelection.selection.end
let text = ""
if (start.line === end.line && start.character === end.character) { if (start.line === end.line && start.character === end.character) {
return `Note: The user opened the file "${editorSelection.filePath}".` text = `Note: The user opened the file "${editorSelection.filePath}".`
} else if (start.line === end.line) {
text = `Note: The user selected line ${start.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
} else {
text = `Note: The user selected lines ${start.line + 1} to ${end.line + 1} from "${editorSelection.filePath}". \`\`\`${editorSelection.text}\`\`\`\n\n`
} }
if (start.line === end.line) {
return `Note: The user selected line ${start.line} from "${editorSelection.filePath}": ${editorSelection.text}` return `<system-reminder>${text} This may or may not be relevant to the current task.</system-reminder>\n`
}
return `Note: The user selected lines ${start.line} to ${end.line} from "${editorSelection.filePath}": ${editorSelection.text}`
})(), })(),
synthetic: true, synthetic: true,
metadata: {
kind: "editor_context",
source: editorSelection.source ?? "editor",
filePath: editorSelection.filePath,
selection: editorSelection.selection,
},
}, },
] ]
: [] : []
@@ -829,6 +840,7 @@ export function Prompt(props: PromptProps) {
], ],
}) })
.catch(() => {}) .catch(() => {})
editor.clearSelection()
} }
history.append({ history.append({
...store.prompt, ...store.prompt,
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js" import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store" import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper" import { createSimpleContext } from "../../context/helper"
@@ -1,7 +1,7 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import type { KeyBinding } from "@opentui/core" import type { KeyBinding } from "@opentui/core"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
const TEXTAREA_ACTIONS = [ const TEXTAREA_ACTIONS = [
"submit", "submit",
@@ -5,7 +5,8 @@ import z from "zod"
import { TuiInfo, TuiOptions } from "./tui-schema" import { TuiInfo, TuiOptions } from "./tui-schema"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem, Log } from "@/util" import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import * as ConfigPaths from "@/config/paths" import * as ConfigPaths from "@/config/paths"
const log = Log.create({ service: "tui.migrate" }) const log = Log.create({ service: "tui.migrate" })
@@ -16,7 +16,8 @@ import { ConfigPlugin } from "@/config/plugin"
import { ConfigKeybinds } from "@/config/keybinds" import { ConfigKeybinds } from "@/config/keybinds"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { Filesystem, Log } from "@/util" import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { ConfigVariable } from "@/config/variable" import { ConfigVariable } from "@/config/variable"
import { Npm } from "@opencode-ai/core/npm" import { Npm } from "@opencode-ai/core/npm"
@@ -2,11 +2,12 @@ import { Database } from "bun:sqlite"
import os from "node:os" import os from "node:os"
import path from "node:path" import path from "node:path"
import z from "zod" import z from "zod"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import type { EditorSelection } from "./editor" import type { EditorSelection } from "./editor"
const ZedEditorRowSchema = z.object({ const ZedEditorRowSchema = z.object({
editor_id: z.number(), item_kind: z.string(),
editor_id: z.number().nullable(),
workspace_id: z.number(), workspace_id: z.number(),
workspace_paths: z.string().nullable(), workspace_paths: z.string().nullable(),
timestamp: z.string(), timestamp: z.string(),
@@ -20,25 +21,41 @@ const ZedEditorContentsSchema = z.object({
}) })
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema> type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
export async function resolveZedSelection(dbPath: string): Promise<EditorSelection | undefined> { export type ZedSelectionResult =
const row = queryZedActiveEditor(dbPath, process.cwd()) | { type: "selection"; selection: EditorSelection }
if (!row?.buffer_path || row.selection_start == null || row.selection_end == null) return | { type: "empty" }
| { type: "unavailable" }
export async function resolveZedSelection(dbPath: string, cwd = process.cwd()): Promise<ZedSelectionResult> {
const active = queryZedActiveEditor(dbPath, cwd)
if (active.type !== "row") return active
const row = active.row
if (!row.buffer_path) return { type: "empty" }
if (row.selection_start == null || row.selection_end == null) return { type: "unavailable" }
const contents = queryZedEditorContents(dbPath, row)
const text = const text =
queryZedEditorContents(dbPath, row) ?? contents.type === "contents" && contents.contents != null
(await Bun.file(row.buffer_path) ? contents.contents
.text() : await Bun.file(row.buffer_path)
.catch(() => undefined)) .text()
if (text == null) return .catch(() => undefined)
if (text == null) return { type: "unavailable" }
const startOffset = Math.min(row.selection_start, row.selection_end) const startOffset = Math.min(row.selection_start, row.selection_end)
const endOffset = Math.max(row.selection_start, row.selection_end) const endOffset = Math.max(row.selection_start, row.selection_end)
return { return {
text: text.slice(startOffset, endOffset), type: "selection",
filePath: row.buffer_path, selection: {
selection: offsetsToSelection(text, startOffset, endOffset), text: text.slice(startOffset, endOffset),
filePath: row.buffer_path,
source: "zed",
selection: offsetsToSelection(text, startOffset, endOffset),
},
} }
} }
@@ -46,11 +63,12 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
let db: Database | undefined let db: Database | undefined
try { try {
db = new Database(dbPath, { readonly: true }) db = new Database(dbPath, { readonly: true })
return db const raw = db
.query( .query(
`select `select
i.kind as item_kind,
e.item_id as editor_id, e.item_id as editor_id,
e.workspace_id as workspace_id, i.workspace_id as workspace_id,
w.paths as workspace_paths, w.paths as workspace_paths,
w.timestamp as timestamp, w.timestamp as timestamp,
e.buffer_path as buffer_path, e.buffer_path as buffer_path,
@@ -59,31 +77,40 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
from items i from items i
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
join workspaces w on w.workspace_id = i.workspace_id join workspaces w on w.workspace_id = i.workspace_id
join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id
where i.active = 1 and p.active = 1 and i.kind = 'Editor' and e.buffer_path is not null where i.active = 1 and p.active = 1
order by w.timestamp desc`, order by w.timestamp desc`,
) )
.all() .all()
.flatMap((row) => {
const parsed = ZedEditorRowSchema.safeParse(row) const rows = raw.flatMap((row) => {
return parsed.success ? [parsed.data] : [] const parsed = ZedEditorRowSchema.safeParse(row)
}) return parsed.success ? [parsed.data] : []
})
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
const row = rows
.map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) })) .map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) }))
.filter((entry) => entry.score > 0) .filter((entry) => entry.score > 0)
.sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
if (!row) return { type: "empty" as const }
if (row.item_kind !== "Editor") return { type: "unavailable" as const }
if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
return { type: "row" as const, row }
} catch { } catch {
return return { type: "unavailable" as const }
} finally { } finally {
db?.close() db?.close()
} }
} }
function queryZedEditorContents(dbPath: string, row: ZedEditorRow) { function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
let db: Database | undefined let db: Database | undefined
try { try {
db = new Database(dbPath, { readonly: true }) db = new Database(dbPath, { readonly: true })
return ZedEditorContentsSchema.safeParse( const parsed = ZedEditorContentsSchema.safeParse(
db db
.query( .query(
`select contents `select contents
@@ -91,14 +118,20 @@ function queryZedEditorContents(dbPath: string, row: ZedEditorRow) {
where item_id = $editorID and workspace_id = $workspaceID`, where item_id = $editorID and workspace_id = $workspaceID`,
) )
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }), .get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
).data?.contents )
if (!parsed.success) return { type: "unavailable" as const }
return { type: "contents" as const, contents: parsed.data.contents }
} catch { } catch {
return return { type: "unavailable" as const }
} finally { } finally {
db?.close() db?.close()
} }
} }
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
return row.item_kind === "Editor" && row.editor_id != null
}
export function resolveZedDbPath() { export function resolveZedDbPath() {
const candidates = [ const candidates = [
process.env.OPENCODE_ZED_DB, process.env.OPENCODE_ZED_DB,
@@ -31,6 +31,7 @@ const PositionSchema = z.object({
const EditorSelectionSchema = z.object({ const EditorSelectionSchema = z.object({
text: z.string(), text: z.string(),
filePath: z.string(), filePath: z.string(),
source: z.enum(["websocket", "zed"]).optional(),
selection: z.object({ selection: z.object({
start: PositionSchema, start: PositionSchema,
end: PositionSchema, end: PositionSchema,
@@ -107,9 +108,11 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
send({ id: requestID, method, params }) send({ id: requestID, method, params })
} }
const scheduleReconnect = (delay: number) => { const scheduleReconnect = () => {
if (closed) return if (closed) return
if (reconnect) clearTimeout(reconnect) if (reconnect) clearTimeout(reconnect)
attempt += 1
const delay = Math.min(1000 * 2 ** (attempt - 1), 10_000)
reconnect = setTimeout(connect, delay) reconnect = setTimeout(connect, delay)
} }
@@ -121,12 +124,14 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
const dbPath = resolveZedDbPath() const dbPath = resolveZedDbPath()
if (!dbPath) { if (!dbPath) {
setStore("status", "disabled") setStore("status", "disabled")
scheduleReconnect(1000) scheduleReconnect()
return return
} }
zedSelection ??= resolveZedSelection(dbPath) zedSelection ??= resolveZedSelection(dbPath)
.then((selection) => { .then((result) => {
if (closed || socket) return if (closed || socket) return
if (result.type === "unavailable") return
const selection = result.type === "selection" ? result.selection : undefined
const key = editorSelectionKey(selection) const key = editorSelectionKey(selection)
if (key !== lastZedSelectionKey) { if (key !== lastZedSelectionKey) {
lastZedSelectionKey = key lastZedSelectionKey = key
@@ -135,13 +140,12 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
} }
}) })
.catch(() => { .catch(() => {
if (closed || socket) return // Keep the last known Zed selection for transient polling failures.
setStore("status", "disabled")
}) })
.finally(() => { .finally(() => {
zedSelection = undefined zedSelection = undefined
}) })
scheduleReconnect(1000) scheduleReconnect()
return return
} }
@@ -171,7 +175,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
const selection = const selection =
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
if (selection?.success) { if (selection?.success) {
setStore("selection", selection.data) setStore("selection", { ...selection.data, source: "websocket" })
return return
} }
@@ -205,13 +209,11 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
if (closed) return if (closed) return
setStore("status", "connecting") setStore("status", "connecting")
attempt += 1 scheduleReconnect()
const delay = Math.min(1000 * 2 ** (attempt - 1), 30000)
scheduleReconnect(delay)
}) })
} }
scheduleReconnect(0) connect()
onCleanup(() => { onCleanup(() => {
closed = true closed = true
@@ -230,6 +232,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
selection() { selection() {
return store.selection return store.selection
}, },
clearSelection() {
setStore("selection", undefined)
},
onMention(listener: (mention: EditorMention) => void) { onMention(listener: (mention: EditorMention) => void) {
mentionListeners.add(listener) mentionListeners.add(listener)
return () => mentionListeners.delete(listener) return () => mentionListeners.delete(listener)
@@ -1,5 +1,5 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import { pipe, mapValues } from "remeda" import { pipe, mapValues } from "remeda"
import type { TuiConfig } from "@/cli/cmd/tui/config/tui" import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
import type { ParsedKey, Renderable } from "@opentui/core" import type { ParsedKey, Renderable } from "@opentui/core"
@@ -1,5 +1,5 @@
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { Flock } from "@opencode-ai/core/util/flock" import { Flock } from "@opencode-ai/core/util/flock"
import { rename, rm } from "fs/promises" import { rename, rm } from "fs/promises"
import { createSignal, type Setter } from "solid-js" import { createSignal, type Setter } from "solid-js"
@@ -11,7 +11,7 @@ import { useToast } from "../ui/toast"
import { useArgs } from "./args" import { useArgs } from "./args"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core" import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
export function parseModel(model: string) { export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/") const [providerID, ...rest] = model.split("/")
@@ -28,7 +28,7 @@ import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit" import { useExit } from "./exit"
import { useArgs } from "./args" import { useArgs } from "./args"
import { batch, onMount } from "solid-js" import { batch, onMount } from "solid-js"
import { Log } from "@/util" import * as Log from "@opencode-ai/core/util/log"
import { emptyConsoleState, type ConsoleState } from "@/config/console-state" import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
@@ -40,7 +40,7 @@ import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid" import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { useTuiConfig } from "./tui-config" import { useTuiConfig } from "./tui-config"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
@@ -1,4 +1,4 @@
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui" import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
@@ -1,3 +0,0 @@
export { TuiPluginRuntime } from "./runtime"
export { createTuiApi } from "./api"
export type { RouteMap } from "./api"
@@ -13,7 +13,7 @@ import {
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { Log } from "@/util" import * as Log from "@opencode-ai/core/util/log"
import { errorData, errorMessage } from "@/util/error" import { errorData, errorMessage } from "@/util/error"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
@@ -30,8 +30,8 @@ import { PluginMeta } from "@/plugin/meta"
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
import { hasTheme, upsertTheme } from "../context/theme" import { hasTheme, upsertTheme } from "../context/theme"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util" import { Process } from "@/util/process"
import { Flock } from "@opencode-ai/core/util/flock" import { Flock } from "@opencode-ai/core/util/flock"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
@@ -8,7 +8,7 @@ import { useArgs } from "../context/args"
import { useRouteData } from "@tui/context/route" import { useRouteData } from "@tui/context/route"
import { usePromptRef } from "../context/prompt" import { usePromptRef } from "../context/prompt"
import { useLocal } from "../context/local" import { useLocal } from "../context/local"
import { TuiPluginRuntime } from "../plugin" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
let once = false let once = false
const placeholder = { const placeholder = {
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2" import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useDialog, type DialogContext } from "../../ui/dialog" import { useDialog, type DialogContext } from "../../ui/dialog"
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2" import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { DialogMessage } from "./dialog-message" import { DialogMessage } from "./dialog-message"
import { useDialog } from "../../ui/dialog" import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "../../component/prompt/history" import type { PromptInfo } from "../../component/prompt/history"
@@ -33,8 +33,8 @@ import type {
ReasoningPart, ReasoningPart,
} from "@opencode-ai/sdk/v2" } from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import type { Tool } from "@/tool" import type { Tool } from "@/tool/tool"
import type { ReadTool } from "@/tool/read" import type { ReadTool } from "@/tool/read"
import type { WriteTool } from "@/tool/write" import type { WriteTool } from "@/tool/write"
import { BashTool } from "@/tool/bash" import { BashTool } from "@/tool/bash"
@@ -75,7 +75,7 @@ import * as Editor from "../../util/editor"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt" import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { PermissionPrompt } from "./permission" import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question" import { QuestionPrompt } from "./question"
@@ -85,7 +85,7 @@ import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts" import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config" import { useTuiConfig } from "../../context/tui-config"
import { getScrollAcceleration } from "../../util/scroll" import { getScrollAcceleration } from "../../util/scroll"
import { TuiPluginRuntime } from "../../plugin" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
import { DialogGoUpsell } from "../../component/dialog-go-upsell" import { DialogGoUpsell } from "../../component/dialog-go-upsell"
import { SessionRetry } from "@/session/retry" import { SessionRetry } from "@/session/retry"
import { getRevertDiffFiles } from "../../util/revert-diff" import { getRevertDiffFiles } from "../../util/revert-diff"
@@ -12,8 +12,8 @@ import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useProject } from "../../context/project" import { useProject } from "../../context/project"
import path from "path" import path from "path"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { useDialog } from "../../ui/dialog" import { useDialog } from "../../ui/dialog"
import { getScrollAcceleration } from "../../util/scroll" import { getScrollAcceleration } from "../../util/scroll"
@@ -4,7 +4,7 @@ import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme" import { useTheme } from "../../context/theme"
import { useTuiConfig } from "../../context/tui-config" import { useTuiConfig } from "../../context/tui-config"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { TuiPluginRuntime } from "../../plugin" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
import { getScrollAcceleration } from "../../util/scroll" import { getScrollAcceleration } from "../../util/scroll"
@@ -6,7 +6,7 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command" import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind" import { useKeybind } from "../../context/keybind"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { useTerminalDimensions } from "@opentui/solid" import { useTerminalDimensions } from "@opentui/solid"
export function SubagentFooter() { export function SubagentFooter() {
+3 -3
View File
@@ -1,15 +1,15 @@
import { cmd } from "@/cli/cmd/cmd" import { cmd } from "@/cli/cmd/cmd"
import { tui } from "./app" import { tui } from "./app"
import { Rpc } from "@/util" import { Rpc } from "@/util/rpc"
import { type rpc } from "./worker" import { type rpc } from "./worker"
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
import { UI } from "@/cli/ui" import { UI } from "@/cli/ui"
import { Log } from "@/util" import * as Log from "@opencode-ai/core/util/log"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
import { withTimeout } from "@/util/timeout" import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
import { Filesystem } from "@/util" import { Filesystem } from "@/util/filesystem"
import type { GlobalEvent } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
@@ -15,6 +15,8 @@ export function DialogAlert(props: DialogAlertProps) {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.() props.onConfirm?.()
dialog.clear() dialog.clear()
} }
@@ -4,7 +4,7 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { For } from "solid-js" import { For } from "solid-js"
import { useKeyboard } from "@opentui/solid" import { useKeyboard } from "@opentui/solid"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
export type DialogConfirmProps = { export type DialogConfirmProps = {
title: string title: string
@@ -25,6 +25,8 @@ export function DialogConfirm(props: DialogConfirmProps) {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
if (store.active === "confirm") props.onConfirm?.() if (store.active === "confirm") props.onConfirm?.()
if (store.active === "cancel") props.onCancel?.() if (store.active === "cancel") props.onCancel?.()
dialog.clear() dialog.clear()
@@ -35,6 +35,8 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.({ props.onConfirm?.({
filename: textarea.plainText, filename: textarea.plainText,
thinking: store.thinking, thinking: store.thinking,
@@ -11,6 +11,8 @@ export function DialogHelp() {
useKeyboard((evt) => { useKeyboard((evt) => {
if (evt.name === "return" || evt.name === "escape") { if (evt.name === "return" || evt.name === "escape") {
evt.preventDefault()
evt.stopPropagation()
dialog.clear() dialog.clear()
} }
}) })
@@ -29,6 +29,8 @@ export function DialogPrompt(props: DialogPromptProps) {
return return
} }
if (evt.name === "return") { if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.(textarea.plainText) props.onConfirm?.(textarea.plainText)
} }
}) })
@@ -8,8 +8,8 @@ import * as fuzzysort from "fuzzysort"
import { isDeepEqual } from "remeda" import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "@tui/ui/dialog" import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { useKeybind } from "@tui/context/keybind" import { useKeybind } from "@tui/context/keybind"
import { Keybind } from "@/util" import { Keybind } from "@/util/keybind"
import { Locale } from "@/util" import { Locale } from "@/util/locale"
import { getScrollAcceleration } from "../util/scroll" import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../context/tui-config" import { useTuiConfig } from "../context/tui-config"

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