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
-atharvau AI review spamming literally every PR
-borealbytes
-carycooper777
-danieljoshuanazareth
-danieljoshuanazareth
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
edemaine
-florianleibert
fwang
@@ -33,4 +35,3 @@ simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr
-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": {
"name": "@opencode-ai/app",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -83,7 +83,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -117,7 +117,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -144,7 +144,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -168,7 +168,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -192,7 +192,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.14.26",
"version": "1.14.28",
"bin": {
"opencode": "./bin/opencode",
},
@@ -226,7 +226,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -259,7 +259,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -303,7 +303,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -332,7 +332,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -348,7 +348,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.26",
"version": "1.14.28",
"bin": {
"opencode": "./bin/opencode",
},
@@ -491,7 +491,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -526,7 +526,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -541,7 +541,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -576,7 +576,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -625,7 +625,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+24
View File
@@ -115,6 +115,27 @@ const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100
appliesToProducts: [zenLiteProduct.id],
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", {
product: zenLiteProduct.id,
currency: "usd",
@@ -131,6 +152,9 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
priceInr: 92900,
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
threeMonths100Coupon: zenLiteCouponThreeMonths100.id,
sixMonths100Coupon: zenLiteCouponSixMonths100.id,
twelveMonths100Coupon: zenLiteCouponTwelveMonths100.id,
},
})
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-zQOuZkuOLm9xV5YzfcQiWSzvussXVw/dRPw4HhuYsdc=",
"aarch64-linux": "sha256-J8Ak9rCCkKlBj0KhdUOklglpR3ntRP2VjtdmEJox6Ro=",
"aarch64-darwin": "sha256-r4F6S9CfGR6qxSaSqu9AIjWCEKd/PuT1Wprcbzk0IwY=",
"x86_64-darwin": "sha256-I/ztb7VnLbJYzAxf2egN0+YsJcP5yix3tIoHtiuRUXE="
"x86_64-linux": "sha256-U/LZx/D+5JTT1LHSyZkEuqXP/ky7LkHrEYBW5pcVArk=",
"aarch64-linux": "sha256-nGZa04h4y3jbdmf87IRrlQm/E5qYR8lj5OxKgQSR2XU=",
"aarch64-darwin": "sha256-GD8pCHWMBppDaIfRKxhY2m4xWo1OrY3wOmGw+EC71mw=",
"x86_64-darwin": "sha256-KOH1ZB8pdpF7Xer6QIH7rrr9fwF/BZkCTJndPe0wypg="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.26",
"version": "1.14.28",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.26",
"version": "1.14.28",
"type": "module",
"license": "MIT",
"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>) {
return (
<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 { config } from "~/config"
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 { useLanguage } from "~/context/language"
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: "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: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 1300, d: "200ms" },
{ 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: "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: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
]
const w = 720
@@ -340,6 +340,9 @@ export default function Home() {
<div>
<IconAlibaba width="24" height="24" />
</div>
<div>
<IconDeepSeek width="24" height="24" />
</div>
<div>
<IconMiMo width="24" height="24" />
</div>
@@ -160,8 +160,16 @@ export async function POST(input: APIEvent) {
userID: userID,
})
if (userEmail && coupon === LiteData.firstMonth100Coupon) {
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
if (userEmail) {
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)
async function getCosts(workspaceID: string, year: number, month: number) {
async function getCosts(workspaceID: string, year: number, month: number, tzOffset: string) {
"use server"
return withActor(async () => {
const startDate = new Date(year, month, 1)
const endDate = new Date(year, month + 1, 1)
const timezoneOffset = (() => {
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) =>
tx
.select({
date: sql<string>`DATE(${UsageTable.timeCreated})`,
date: dateExpr,
model: UsageTable.model,
totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID,
@@ -42,16 +50,11 @@ async function getCosts(workspaceID: string, year: number, month: number) {
.where(
and(
eq(UsageTable.workspaceID, workspaceID),
gte(UsageTable.timeCreated, startDate),
lt(UsageTable.timeCreated, endDate),
gte(UsageTable.timeCreated, monthStartUTC),
lt(UsageTable.timeCreated, monthEndUTC),
),
)
.groupBy(
sql`DATE(${UsageTable.timeCreated})`,
UsageTable.model,
UsageTable.keyID,
sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
)
.groupBy(dateExpr, UsageTable.model, UsageTable.keyID, sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`)
.then((x) =>
x.map((r) => ({
...r,
@@ -125,15 +128,45 @@ function getModelColor(model: string): string {
}
function formatDateLabel(dateStr: string): string {
const date = new Date()
const [y, m, d] = dateStr.split("-").map(Number)
date.setFullYear(y)
date.setMonth(m - 1)
date.setDate(d)
date.setHours(0, 0, 0, 0)
const month = date.toLocaleDateString(undefined, { month: "short" })
const day = date.getUTCDate().toString().padStart(2, "0")
return `${month} ${day}`
const [, m, d] = dateStr.split("-").map(Number)
const month = new Date(2000, m - 1, 1).toLocaleDateString(undefined, { month: "short" })
return `${month} ${d.toString().padStart(2, "0")}`
}
// Compute the UTC offset (in MySQL CONVERT_TZ format like "+05:30") for the
// given IANA timezone at the given instant. Honors DST.
function getTimezoneOffset(timezone: string, at: Date): string {
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 {
@@ -152,6 +185,7 @@ export function GraphSection() {
let chartInstance: Chart | undefined
const params = useParams()
const i18n = useI18n()
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
const now = new Date()
const [store, setStore] = createStore({
data: null as Awaited<ReturnType<typeof getCosts>> | null,
@@ -185,10 +219,13 @@ export function GraphSection() {
})
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) => {
const date = new Date(store.year, store.month, i + 1)
return date.toISOString().split("T")[0]
const dd = (i + 1).toString().padStart(2, "0")
return `${yyyy}-${mm}-${dd}`
})
})
@@ -415,7 +452,11 @@ export function GraphSection() {
})
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 })
})
@@ -1,10 +1,12 @@
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) {
return optionsHandler()
return buildOptionsResponse()
}
export async function GET(input: APIEvent) {
return getHandler({ modelList: "lite" })
export async function GET(_input: APIEvent) {
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 optionsHandler() {
export async function buildOptionsResponse() {
return new Response(null, {
status: 200,
headers: {
@@ -11,16 +9,13 @@ export async function optionsHandler() {
})
}
export async function getHandler(opts: { modelList: "lite" | "full"; disabledModels?: string[] }) {
const zenData = ZenData.list(opts.modelList)
export async function buildModelsResponse(models: string[]) {
return new Response(
JSON.stringify({
object: "list",
data: Object.entries(zenData.models)
.filter(([id]) => !opts.disabledModels?.includes(id))
.filter(([id]) => !id.startsWith("alpha-"))
.map(([id, _model]) => ({
data: models
.filter((id) => !id.startsWith("alpha-"))
.map((id) => ({
id,
object: "model",
created: Math.floor(Date.now() / 1000),
@@ -1,18 +1,19 @@
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 { KeyTable } from "@opencode-ai/console-core/schema/key.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 { optionsHandler, getHandler } from "~/routes/zen/util/modelsHandler"
import { buildOptionsResponse, buildModelsResponse } from "~/routes/zen/util/modelsHandler"
export async function OPTIONS(_input: APIEvent) {
return optionsHandler()
return buildOptionsResponse()
}
export async function GET(input: APIEvent) {
const disabledModels = await (() => {
const apiKey = input.request.headers.get("authorization")?.split(" ")[1]
if (!apiKey) return []
if (!apiKey) return [] as string[]
return Database.use((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",
"name": "@opencode-ai/console-core",
"version": "1.14.26",
"version": "1.14.28",
"private": true,
"type": "module",
"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) =>
tx
.select()
.select({ type: CouponTable.type, timeRedeemed: CouponTable.timeRedeemed })
.from(CouponTable)
.where(and(eq(CouponTable.email, email), eq(CouponTable.type, type), isNull(CouponTable.timeRedeemed)))
.then((rows) => rows.length > 0),
.where(and(eq(CouponTable.email, email), isNull(CouponTable.timeRedeemed)))
.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.liteSubscriptionID) throw new Error("Already subscribed to Lite")
const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH"))
? LiteData.firstMonth100Coupon
: LiteData.firstMonth50Coupon
const coupons = await Billing.getCoupons(email)
const coupon = coupons.includes("GO12MONTHS100")
? LiteData.twelveMonths100Coupon
: coupons.includes("GO6MONTHS100")
? LiteData.sixMonths100Coupon
: coupons.includes("GO3MONTHS100")
? LiteData.threeMonths100Coupon
: coupons.includes("GOFREEMONTH")
? LiteData.firstMonth100Coupon
: LiteData.firstMonth50Coupon
const createSession = () =>
Billing.stripe().checkout.sessions.create({
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 firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon
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")
}
@@ -133,7 +133,7 @@ export const UsageTable = mysqlTable(
(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(
"coupon",
{
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string
"priceInr": number
"product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable"
}
"ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.26",
"version": "1.14.28",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string
"priceInr": number
"product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable"
}
"ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.26",
"version": "1.14.28",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string
"priceInr": number
"product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable"
}
"ZEN_MODELS1": {
+2 -1
View File
@@ -1,12 +1,13 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.26",
"version": "1.14.28",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
"private": true,
"scripts": {
"test": "bun test",
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
"typecheck": "tsgo --noEmit"
},
"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"
import path from "path"
import { fileURLToPath } from "url"
import npa from "npm-package-arg"
import semver from "semver"
// @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 { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { AppFileSystem } from "./filesystem"
import { Global } from "./global"
import { EffectFlock } from "./util/effect-flock"
import { makeRuntime } from "./effect/runtime"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
import { NpmConfig } from "./npm-config"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional),
@@ -40,46 +32,18 @@ export interface Interface {
}[]
},
) => 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>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
const npmPath = fileURLToPath(new URL("..", import.meta.url))
export function sanitize(pkg: string) {
if (!illegal) return pkg
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 => {
let entrypoint: Option.Option<string>
try {
@@ -110,39 +74,13 @@ export const layer = Layer.effect(
const global = yield* Global.Service
const fs = yield* FileSystem.FileSystem
const flock = yield* EffectFlock.Service
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
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[] }) =>
Effect.gen(function* () {
yield* flock.acquire(`npm-install:${input.dir}`)
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
const add = input.add ?? []
const npmOptions = yield* loadOptions(input.dir)
const npmOptions = yield* NpmConfig.load(input.dir)
const arborist = new Arborist({
...npmOptions,
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 dir = directory(pkg)
const name = (() => {
@@ -309,7 +235,6 @@ export const layer = Layer.effect(
return Service.of({
add,
install,
outdated,
which,
})
}),
@@ -320,7 +245,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(AppFileSystem.layer),
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
Layer.provide(CrossSpawnSpawner.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"]>) {
const resolved = await runPromise((svc) => svc.which(...args))
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",
"private": true,
"version": "1.14.26",
"version": "1.14.28",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.26",
"version": "1.14.28",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.26",
"version": "1.14.28",
"private": true,
"type": "module",
"license": "MIT",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string
"priceInr": number
"product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable"
}
"ZEN_MODELS1": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.26"
version = "1.14.28"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[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"
args = ["acp"]
[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"
args = ["acp"]
[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"
args = ["acp"]
[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"
args = ["acp"]
[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"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.26",
"version": "1.14.28",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3
View File
@@ -148,6 +148,9 @@ declare module "sst" {
"price": string
"priceInr": number
"product": string
"sixMonths100Coupon": string
"threeMonths100Coupon": string
"twelveMonths100Coupon": string
"type": "sst.sst.Linkable"
}
"ZEN_MODELS1": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.26",
"version": "1.14.28",
"name": "opencode",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
import { z } from "zod"
import { Config } from "../src/config"
import { Config } from "@/config/config"
import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
function generate(schema: z.ZodType) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage"
import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url"
+4 -4
View File
@@ -31,19 +31,19 @@ import {
type Usage,
} from "@agentclientprotocol/sdk"
import { Log } from "../util"
import * as Log from "@opencode-ai/core/util/log"
import { pathToFileURL } from "url"
import { Filesystem } from "../util"
import { Filesystem } from "@/util/filesystem"
import { Hash } from "@opencode-ai/core/util/hash"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
import { Provider } from "../provider"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { Agent as AgentModule } from "../agent/agent"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config"
import { Config } from "@/config/config"
import { ConfigMCP } from "@/config/mcp"
import { Todo } from "@/session/todo"
import { Result, Schema } from "effect"
+1 -1
View File
@@ -1,6 +1,6 @@
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
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"
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 { Provider } from "../provider"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
import { Truncate } from "../tool"
import { Truncate } from "@/tool/truncate"
import { Auth } from "../auth"
import { ProviderTransform } from "../provider"
import { ProviderTransform } from "@/provider/transform"
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
@@ -19,7 +19,7 @@ import path from "path"
import { Plugin } from "@/plugin"
import { Skill } from "../skill"
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 OtelTracer from "@effect/opentelemetry/Tracer"
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 { EffectBridge } from "@/effect"
import { Log } from "../util"
import { EffectBridge } from "@/effect/bridge"
import * as Log from "@opencode-ai/core/util/log"
import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
import { InstanceState } from "@/effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
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 { cmd } from "./cmd"
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 { Global } from "@opencode-ai/core/global"
import { Agent } from "../../agent/agent"
import { Provider } from "../../provider"
import { Provider } from "@/provider/provider"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../util"
import { Filesystem } from "@/util/filesystem"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { EOL } from "os"
@@ -15,7 +15,23 @@ import type { Argv } from "yargs"
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({
command: "create",
@@ -35,9 +51,10 @@ const AgentCreateCommand = cmd({
describe: "agent mode",
choices: ["all", "primary", "subagent"] as const,
})
.option("tools", {
.option("permissions", {
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", {
type: "string",
@@ -51,9 +68,9 @@ const AgentCreateCommand = cmd({
const cliPath = args.path
const cliDescription = args.description
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) {
UI.empty()
@@ -120,21 +137,21 @@ const AgentCreateCommand = cmd({
})
spinner.stop(`Agent ${generated.identifier} generated`)
// Select tools
let selectedTools: string[]
if (cliTools !== undefined) {
selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
// Select permissions to allow
let selected: string[]
if (perms !== undefined) {
selected = perms ? perms.split(",").map((t) => t.trim()) : AVAILABLE_PERMISSIONS
} else {
const result = await prompts.multiselect({
message: "Select tools to enable (Space to toggle)",
options: AVAILABLE_TOOLS.map((tool) => ({
label: tool,
value: tool,
message: "Select permissions to allow (Space to toggle)",
options: AVAILABLE_PERMISSIONS.map((permission) => ({
label: permission,
value: permission,
})),
initialValues: AVAILABLE_TOOLS,
initialValues: AVAILABLE_PERMISSIONS,
})
if (prompts.isCancel(result)) throw new UI.CancelledError()
selectedTools = result
selected = result
}
// Get mode
@@ -167,11 +184,11 @@ const AgentCreateCommand = cmd({
mode = modeResult
}
// Build tools config
const tools: Record<string, boolean> = {}
for (const tool of AVAILABLE_TOOLS) {
if (!selectedTools.includes(tool)) {
tools[tool] = false
// Build permissions config — deny anything not explicitly selected.
const permissions: Record<string, "deny"> = {}
for (const permission of AVAILABLE_PERMISSIONS) {
if (!selected.includes(permission)) {
permissions[permission] = "deny"
}
}
@@ -179,13 +196,13 @@ const AgentCreateCommand = cmd({
const frontmatter: {
description: string
mode: AgentMode
tools?: Record<string, boolean>
permission?: Record<string, "deny">
} = {
description: generated.whenToUse,
mode,
}
if (Object.keys(tools).length > 0) {
frontmatter.tools = tools
if (Object.keys(permissions).length > 0) {
frontmatter.permission = permissions
}
// Write file
+2 -2
View File
@@ -1,11 +1,11 @@
import type { Argv } from "yargs"
import { spawn } from "child_process"
import { Database } from "../../storage"
import { Database } from "@/storage/db"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { JsonMigration } from "../../storage"
import { JsonMigration } from "@/storage/json-migration"
import { EOL } from "os"
import { errorMessage } from "../../util/error"
+3 -3
View File
@@ -2,11 +2,11 @@ import { EOL } from "os"
import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider"
import { Session } from "../../../session"
import { Provider } from "@/provider/provider"
import { Session } from "@/session/session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
import { ToolRegistry } from "../../../tool"
import { ToolRegistry } from "@/tool/registry"
import { Instance } from "../../../project/instance"
import { Permission } from "../../../permission"
import { iife } from "../../../util/iife"
@@ -1,5 +1,5 @@
import { EOL } from "os"
import { Config } from "../../../config"
import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { bootstrap } from "../../bootstrap"
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 { Effect } from "effect"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { Log } from "../../../util"
import * as Log from "@opencode-ai/core/util/log"
import { EOL } from "os"
export const LSPCommand = cmd({
+2 -2
View File
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Project } from "../../../project"
import { Log } from "../../../util"
import { Project } from "@/project/project"
import * as Log from "@opencode-ai/core/util/log"
import { cmd } from "../cmd"
export const ScrapCommand = cmd({
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Argv } from "yargs"
import { Session } from "../../session"
import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema"
import { cmd } from "./cmd"
+6 -6
View File
@@ -1,6 +1,6 @@
import path from "path"
import { exec } from "child_process"
import { Filesystem } from "../../util"
import { Filesystem } from "@/util/filesystem"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest"
@@ -18,21 +18,21 @@ import type {
} from "@octokit/webhooks-types"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { ModelsDev } from "../../provider"
import { ModelsDev } from "@/provider/models"
import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share"
import { Session } from "../../session"
import { SessionShare } from "@/share/session"
import { Session } from "@/session/session"
import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema"
import { Provider } from "../../provider"
import { Provider } from "@/provider/provider"
import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util"
import { Process } from "@/util/process"
import { Effect } from "effect"
type GitHubAuthor = {
+4 -4
View File
@@ -1,15 +1,15 @@
import type { Argv } from "yargs"
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 { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage"
import { Database } from "@/storage/db"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Instance } from "../../project/instance"
import { ShareNext } from "../../share"
import { ShareNext } from "@/share/share-next"
import { EOL } from "os"
import { Filesystem } from "../../util"
import { Filesystem } from "@/util/filesystem"
import { AppRuntime } from "@/effect/app-runtime"
import { Schema } from "effect"
+2 -2
View File
@@ -7,7 +7,7 @@ import { UI } from "../ui"
import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "../../config"
import { Config } from "@/config/config"
import { ConfigMCP } from "../../config/mcp"
import { Instance } from "../../project/instance"
import { Installation } from "../../installation"
@@ -15,7 +15,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path"
import { Global } from "@opencode-ai/core/global"
import { modify, applyEdits } from "jsonc-parser"
import { Filesystem } from "../../util"
import { Filesystem } from "@/util/filesystem"
import { Bus } from "../../bus"
import { AppRuntime } from "../../effect/app-runtime"
import { Effect } from "effect"
+2 -2
View File
@@ -1,8 +1,8 @@
import type { Argv } from "yargs"
import { Instance } from "../../project/instance"
import { Provider } from "../../provider"
import { Provider } from "@/provider/provider"
import { ProviderID } from "../../provider/schema"
import { ModelsDev } from "../../provider"
import { ModelsDev } from "@/provider/models"
import { cmd } from "./cmd"
import { UI } from "../ui"
import { EOL } from "os"
+3 -3
View File
@@ -1,14 +1,14 @@
import { intro, log, outro, spinner } from "@clack/prompts"
import type { Argv } from "yargs"
import { ConfigPaths } from "../../config"
import { ConfigPaths } from "@/config/paths"
import { Global } from "@opencode-ai/core/global"
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
import { resolvePluginTarget } from "../../plugin/shared"
import { Instance } from "../../project/instance"
import { errorMessage } from "../../util/error"
import { Filesystem } from "../../util"
import { Process } from "../../util"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { UI } from "../ui"
import { cmd } from "./cmd"
+1 -1
View File
@@ -3,7 +3,7 @@ import { cmd } from "./cmd"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git"
import { Instance } from "@/project/instance"
import { Process } from "@/util"
import { Process } from "@/util/process"
export const PrCommand = cmd({
command: "pr <number>",
+3 -3
View File
@@ -3,16 +3,16 @@ import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { ModelsDev } from "../../provider"
import { ModelsDev } from "@/provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Config } from "../../config"
import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util"
import { Process } from "@/util/process"
import { text } from "node:stream/consumers"
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 { bootstrap } from "../bootstrap"
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 { Server } from "../../server/server"
import { Provider } from "../../provider"
import { Provider } from "@/provider/provider"
import { Agent } from "../../agent/agent"
import { Permission } from "../../permission"
import { Tool } from "../../tool"
import { Tool } from "@/tool/tool"
import { GlobTool } from "../../tool/glob"
import { GrepTool } from "../../tool/grep"
import { ReadTool } from "../../tool/read"
@@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task"
import { SkillTool } from "../../tool/skill"
import { BashTool } from "../../tool/bash"
import { TodoWriteTool } from "../../tool/todo"
import { Locale } from "../../util"
import { Locale } from "@/util/locale"
import { AppRuntime } from "@/effect/app-runtime"
type ToolProps<T> = {
+4 -4
View File
@@ -1,13 +1,13 @@
import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
import { Session } from "@/session/session"
import { SessionID } from "../../session/schema"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import { Locale } from "../../util"
import { Locale } from "@/util/locale"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Filesystem } from "../../util"
import { Process } from "../../util"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { EOL } from "os"
import path from "path"
import { which } from "../../util/which"
+3 -3
View File
@@ -1,10 +1,10 @@
import type { Argv } from "yargs"
import { cmd } from "./cmd"
import { Session } from "../../session"
import { Session } from "@/session/session"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage"
import { Database } from "@/storage/db"
import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project"
import { Project } from "@/project/project"
import { Instance } from "../../project/instance"
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 { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session"
import { Session as SessionApi } from "@/session/session"
import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv"
import { Provider } from "@/provider"
import { Provider } from "@/provider/provider"
import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
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 type { EventSource } from "./context/sdk"
@@ -725,6 +727,15 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
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",
value: "app.toggle.diffwrap",
@@ -77,6 +77,8 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
return
}
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
if (selected() === "subscribe") subscribe(props, dialog)
else dismiss(props, dialog)
}
@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
import { map, pipe, entries, sortBy } from "remeda"
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useTheme } from "../context/theme"
import { Keybind } from "@/util"
import { Keybind } from "@/util/keybind"
import { TextAttributes } from "@opentui/core"
import { useSDK } from "@tui/context/sdk"
@@ -42,6 +42,8 @@ export function DialogSessionDeleteFailed(props: {
useKeyboard((evt) => {
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
void confirm()
}
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 { useSync } from "@tui/context/sync"
import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { useProject } from "@tui/context/project"
import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { Flag } from "@opencode-ai/core/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename"
import { Keybind } from "@/util"
import { Keybind } from "@/util/keybind"
import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
@@ -1,7 +1,7 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { createMemo, createSignal } from "solid-js"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { useTheme } from "../context/theme"
import { useKeybind } from "../context/keybind"
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 { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
import { useFrecency } from "./frecency"
@@ -1,6 +1,6 @@
import path from "path"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "../../context/helper"
@@ -1,6 +1,6 @@
import path from "path"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper"
@@ -3,7 +3,7 @@ import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, S
import "opentui-spinner/solid"
import path from "path"
import { fileURLToPath } from "url"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { useLocal } from "@tui/context/local"
import { tint, useTheme } from "@tui/context/theme"
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 { TuiEvent } from "../../event"
import { iife } from "@/util/iife"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { formatDuration } from "@/util/format"
import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog"
@@ -112,9 +112,10 @@ export function Prompt(props: PromptProps) {
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
const list = createMemo(() => props.placeholders?.normal ?? [])
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 selection = editor.selection()?.selection
const selection = fileContextEnabled() ? editor.selection()?.selection : undefined
if (!selection) 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}`
@@ -746,7 +747,7 @@ export function Prompt(props: PromptProps) {
// Capture mode before it gets reset
const currentMode = store.mode
const variant = local.model.variant.current()
const editorSelection = editor.selection()
const editorSelection = fileContextEnabled() ? editor.selection() : undefined
const editorParts = editorSelection
? [
{
@@ -755,15 +756,25 @@ export function Prompt(props: PromptProps) {
text: (() => {
const start = editorSelection.selection.start
const end = editorSelection.selection.end
let text = ""
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 `Note: The user selected lines ${start.line} to ${end.line} from "${editorSelection.filePath}": ${editorSelection.text}`
return `<system-reminder>${text} This may or may not be relevant to the current task.</system-reminder>\n`
})(),
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(() => {})
editor.clearSelection()
}
history.append({
...store.prompt,
@@ -1,6 +1,6 @@
import path from "path"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore, produce, unwrap } from "solid-js/store"
import { createSimpleContext } from "../../context/helper"
@@ -1,7 +1,7 @@
import { createMemo } from "solid-js"
import type { KeyBinding } from "@opentui/core"
import { useKeybind } from "../context/keybind"
import { Keybind } from "@/util"
import { Keybind } from "@/util/keybind"
const TEXTAREA_ACTIONS = [
"submit",
@@ -5,7 +5,8 @@ import z from "zod"
import { TuiInfo, TuiOptions } from "./tui-schema"
import { Flag } from "@opencode-ai/core/flag/flag"
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"
const log = Log.create({ service: "tui.migrate" })
@@ -16,7 +16,8 @@ import { ConfigPlugin } from "@/config/plugin"
import { ConfigKeybinds } from "@/config/keybinds"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
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 { Npm } from "@opencode-ai/core/npm"
@@ -2,11 +2,12 @@ import { Database } from "bun:sqlite"
import os from "node:os"
import path from "node:path"
import z from "zod"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import type { EditorSelection } from "./editor"
const ZedEditorRowSchema = z.object({
editor_id: z.number(),
item_kind: z.string(),
editor_id: z.number().nullable(),
workspace_id: z.number(),
workspace_paths: z.string().nullable(),
timestamp: z.string(),
@@ -20,25 +21,41 @@ const ZedEditorContentsSchema = z.object({
})
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
export async function resolveZedSelection(dbPath: string): Promise<EditorSelection | undefined> {
const row = queryZedActiveEditor(dbPath, process.cwd())
if (!row?.buffer_path || row.selection_start == null || row.selection_end == null) return
export type ZedSelectionResult =
| { type: "selection"; selection: EditorSelection }
| { 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 =
queryZedEditorContents(dbPath, row) ??
(await Bun.file(row.buffer_path)
.text()
.catch(() => undefined))
if (text == null) return
contents.type === "contents" && contents.contents != null
? contents.contents
: await Bun.file(row.buffer_path)
.text()
.catch(() => undefined)
if (text == null) return { type: "unavailable" }
const startOffset = Math.min(row.selection_start, row.selection_end)
const endOffset = Math.max(row.selection_start, row.selection_end)
return {
text: text.slice(startOffset, endOffset),
filePath: row.buffer_path,
selection: offsetsToSelection(text, startOffset, endOffset),
type: "selection",
selection: {
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
try {
db = new Database(dbPath, { readonly: true })
return db
const raw = db
.query(
`select
i.kind as item_kind,
e.item_id as editor_id,
e.workspace_id as workspace_id,
i.workspace_id as workspace_id,
w.paths as workspace_paths,
w.timestamp as timestamp,
e.buffer_path as buffer_path,
@@ -59,31 +77,40 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
from items i
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 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
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`,
)
.all()
.flatMap((row) => {
const parsed = ZedEditorRowSchema.safeParse(row)
return parsed.success ? [parsed.data] : []
})
const rows = raw.flatMap((row) => {
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) }))
.filter((entry) => entry.score > 0)
.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 {
return
return { type: "unavailable" as const }
} finally {
db?.close()
}
}
function queryZedEditorContents(dbPath: string, row: ZedEditorRow) {
function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
let db: Database | undefined
try {
db = new Database(dbPath, { readonly: true })
return ZedEditorContentsSchema.safeParse(
const parsed = ZedEditorContentsSchema.safeParse(
db
.query(
`select contents
@@ -91,14 +118,20 @@ function queryZedEditorContents(dbPath: string, row: ZedEditorRow) {
where item_id = $editorID and workspace_id = $workspaceID`,
)
.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 {
return
return { type: "unavailable" as const }
} finally {
db?.close()
}
}
function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
return row.item_kind === "Editor" && row.editor_id != null
}
export function resolveZedDbPath() {
const candidates = [
process.env.OPENCODE_ZED_DB,
@@ -31,6 +31,7 @@ const PositionSchema = z.object({
const EditorSelectionSchema = z.object({
text: z.string(),
filePath: z.string(),
source: z.enum(["websocket", "zed"]).optional(),
selection: z.object({
start: PositionSchema,
end: PositionSchema,
@@ -107,9 +108,11 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
send({ id: requestID, method, params })
}
const scheduleReconnect = (delay: number) => {
const scheduleReconnect = () => {
if (closed) return
if (reconnect) clearTimeout(reconnect)
attempt += 1
const delay = Math.min(1000 * 2 ** (attempt - 1), 10_000)
reconnect = setTimeout(connect, delay)
}
@@ -121,12 +124,14 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
const dbPath = resolveZedDbPath()
if (!dbPath) {
setStore("status", "disabled")
scheduleReconnect(1000)
scheduleReconnect()
return
}
zedSelection ??= resolveZedSelection(dbPath)
.then((selection) => {
.then((result) => {
if (closed || socket) return
if (result.type === "unavailable") return
const selection = result.type === "selection" ? result.selection : undefined
const key = editorSelectionKey(selection)
if (key !== lastZedSelectionKey) {
lastZedSelectionKey = key
@@ -135,13 +140,12 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
}
})
.catch(() => {
if (closed || socket) return
setStore("status", "disabled")
// Keep the last known Zed selection for transient polling failures.
})
.finally(() => {
zedSelection = undefined
})
scheduleReconnect(1000)
scheduleReconnect()
return
}
@@ -171,7 +175,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
const selection =
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
if (selection?.success) {
setStore("selection", selection.data)
setStore("selection", { ...selection.data, source: "websocket" })
return
}
@@ -205,13 +209,11 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
if (closed) return
setStore("status", "connecting")
attempt += 1
const delay = Math.min(1000 * 2 ** (attempt - 1), 30000)
scheduleReconnect(delay)
scheduleReconnect()
})
}
scheduleReconnect(0)
connect()
onCleanup(() => {
closed = true
@@ -230,6 +232,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
selection() {
return store.selection
},
clearSelection() {
setStore("selection", undefined)
},
onMention(listener: (mention: EditorMention) => void) {
mentionListeners.add(listener)
return () => mentionListeners.delete(listener)
@@ -1,5 +1,5 @@
import { createMemo } from "solid-js"
import { Keybind } from "@/util"
import { Keybind } from "@/util/keybind"
import { pipe, mapValues } from "remeda"
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
import type { ParsedKey, Renderable } from "@opentui/core"
@@ -1,5 +1,5 @@
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 { rename, rm } from "fs/promises"
import { createSignal, type Setter } from "solid-js"
@@ -11,7 +11,7 @@ import { useToast } from "../ui/toast"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
@@ -28,7 +28,7 @@ import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit"
import { useArgs } from "./args"
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"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
@@ -40,7 +40,7 @@ import { useKV } from "./kv"
import { useRenderer } from "@opentui/solid"
import { createStore, produce } from "solid-js/store"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { useTuiConfig } from "./tui-config"
import { isRecord } from "@/util/record"
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 { useKeyboard, useTerminalDimensions } from "@opentui/solid"
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 { fileURLToPath } from "url"
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 { isRecord } from "@/util/record"
import { Instance } from "@/project/instance"
@@ -30,8 +30,8 @@ import { PluginMeta } from "@/plugin/meta"
import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install"
import { hasTheme, upsertTheme } from "../context/theme"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util"
import { Process } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"
import { Flock } from "@opencode-ai/core/util/flock"
import { Flag } from "@opencode-ai/core/flag/flag"
import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
@@ -8,7 +8,7 @@ import { useArgs } from "../context/args"
import { useRouteData } from "@tui/context/route"
import { usePromptRef } from "../context/prompt"
import { useLocal } from "../context/local"
import { TuiPluginRuntime } from "../plugin"
import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
let once = false
const placeholder = {
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useDialog, type DialogContext } from "../../ui/dialog"
@@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { DialogMessage } from "./dialog-message"
import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "../../component/prompt/history"
@@ -33,8 +33,8 @@ import type {
ReasoningPart,
} from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util"
import type { Tool } from "@/tool"
import { Locale } from "@/util/locale"
import type { Tool } from "@/tool/tool"
import type { ReadTool } from "@/tool/read"
import type { WriteTool } from "@/tool/write"
import { BashTool } from "@/tool/bash"
@@ -75,7 +75,7 @@ import * as Editor from "../../util/editor"
import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt"
import { useExit } from "../../context/exit"
import { Filesystem } from "@/util"
import { Filesystem } from "@/util/filesystem"
import { Global } from "@opencode-ai/core/global"
import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
@@ -85,7 +85,7 @@ import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
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 { SessionRetry } from "@/session/retry"
import { getRevertDiffFiles } from "../../util/revert-diff"
@@ -12,8 +12,8 @@ import { useTextareaKeybindings } from "../../component/textarea-keybindings"
import { useProject } from "../../context/project"
import path from "path"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import { Keybind } from "@/util"
import { Locale } from "@/util"
import { Keybind } from "@/util/keybind"
import { Locale } from "@/util/locale"
import { Global } from "@opencode-ai/core/global"
import { useDialog } from "../../ui/dialog"
import { getScrollAcceleration } from "../../util/scroll"
@@ -4,7 +4,7 @@ import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme"
import { useTuiConfig } from "../../context/tui-config"
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"
@@ -6,7 +6,7 @@ import { SplitBorder } from "@tui/component/border"
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useKeybind } from "../../context/keybind"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
import { useTerminalDimensions } from "@opentui/solid"
export function SubagentFooter() {
+3 -3
View File
@@ -1,15 +1,15 @@
import { cmd } from "@/cli/cmd/cmd"
import { tui } from "./app"
import { Rpc } from "@/util"
import { Rpc } from "@/util/rpc"
import { type rpc } from "./worker"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import { Log } from "@/util"
import * as Log from "@opencode-ai/core/util/log"
import { errorMessage } from "@/util/error"
import { withTimeout } from "@/util/timeout"
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 { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
@@ -15,6 +15,8 @@ export function DialogAlert(props: DialogAlertProps) {
useKeyboard((evt) => {
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.()
dialog.clear()
}
@@ -4,7 +4,7 @@ import { useDialog, type DialogContext } from "./dialog"
import { createStore } from "solid-js/store"
import { For } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { Locale } from "@/util"
import { Locale } from "@/util/locale"
export type DialogConfirmProps = {
title: string
@@ -25,6 +25,8 @@ export function DialogConfirm(props: DialogConfirmProps) {
useKeyboard((evt) => {
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
if (store.active === "confirm") props.onConfirm?.()
if (store.active === "cancel") props.onCancel?.()
dialog.clear()
@@ -35,6 +35,8 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
useKeyboard((evt) => {
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.({
filename: textarea.plainText,
thinking: store.thinking,
@@ -11,6 +11,8 @@ export function DialogHelp() {
useKeyboard((evt) => {
if (evt.name === "return" || evt.name === "escape") {
evt.preventDefault()
evt.stopPropagation()
dialog.clear()
}
})
@@ -29,6 +29,8 @@ export function DialogPrompt(props: DialogPromptProps) {
return
}
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
props.onConfirm?.(textarea.plainText)
}
})
@@ -8,8 +8,8 @@ import * as fuzzysort from "fuzzysort"
import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { useKeybind } from "@tui/context/keybind"
import { Keybind } from "@/util"
import { Locale } from "@/util"
import { Keybind } from "@/util/keybind"
import { Locale } from "@/util/locale"
import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../context/tui-config"

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