diff --git a/bun.lock b/bun.lock index dc3f6b62..d1f6cfd2 100644 --- a/bun.lock +++ b/bun.lock @@ -114,7 +114,6 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@pierre/precision-diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", @@ -141,7 +140,6 @@ "@types/luxon": "3.7.1", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", - "opencode": "workspace:*", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-icons-spritesheet": "3.0.1", @@ -281,17 +279,24 @@ "version": "0.15.29", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/sdk": "workspace:*", "@pierre/precision-diffs": "catalog:", + "@shikijs/transformers": "3.9.2", "@solidjs/meta": "catalog:", + "@typescript/native-preview": "catalog:", "fuzzysort": "catalog:", "luxon": "catalog:", + "marked": "16.2.0", + "marked-shiki": "1.2.1", "remeda": "catalog:", + "shiki": "3.9.2", "solid-js": "catalog:", "solid-list": "catalog:", "virtua": "catalog:", }, "devDependencies": { "@tailwindcss/vite": "catalog:", + "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", @@ -348,7 +353,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.3.6", + "@pierre/precision-diffs": "0.4.1", "@solidjs/meta": "0.29.4", "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", @@ -942,7 +947,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.3.6", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-cKM3HcMmyr5wPFll0bHYcgHplcHgMlL6Dw4Pi4giL0jVt7ySlGwwVyXTRFW5Fva43stOL+EWB+9U5VBDSktBJA=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.4.1", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/transformers": "3.13.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.13.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-AoozHakINGyNJFgbYc/1PlDK0yunrAxbtXEMBe9fdu8RLkNjVtYRTLw7EF2mM/YuVoVRjj2HT/2VJ4a2rMyDOA=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -1080,7 +1085,7 @@ "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - "@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + "@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-kUTRVKPsB/28H5Ko6qEsyudBiWEDLst+Sfi+hwr59E0GLHV0h8RfgbQU7fdN5Lt9A8R1ulRiZyTvAizkROjwDA=="], @@ -3518,6 +3523,8 @@ "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], + "@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + "@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="], "@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], @@ -3530,10 +3537,6 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@shikijs/core/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], - - "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], - "@slack/bolt/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="], @@ -3798,8 +3801,6 @@ "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "shiki/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], - "sitemap/sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -3954,6 +3955,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], "@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], @@ -4088,6 +4091,8 @@ "@opencode-ai/web/shiki/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + "@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], + "@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], "@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], @@ -4350,6 +4355,8 @@ "@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], + "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], diff --git a/package.json b/package.json index 32e974f5..2a0c07d8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.3.6", + "@pierre/precision-diffs": "0.4.1", "@solidjs/meta": "0.29.4", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index e68b89ae..e1cea636 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -41,7 +41,7 @@ export function Header(props: { zen?: boolean }) { notation: "compact", compactDisplay: "short", }).format(githubData()?.stars!) - : "25K", + : "29K", ) const [store, setStore] = createStore({ diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts index 3dc00adb..e33737d5 100644 --- a/packages/console/app/src/routes/api/enterprise.ts +++ b/packages/console/app/src/routes/api/enterprise.ts @@ -26,7 +26,7 @@ export async function POST(event: APIEvent) { // Create email content const emailContent = ` ${body.message}

--- +--
${body.name}
${body.role}
${body.email}`.trim() diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx index 5bca6f38..4af0ccce 100644 --- a/packages/console/app/src/routes/enterprise/index.tsx +++ b/packages/console/app/src/routes/enterprise/index.tsx @@ -65,94 +65,95 @@ export default function Enterprise() {

Your code is yours

OpenCode operates securely inside your organization with no data or context stored - and no licensing restrictions or ownership claims. Start a trial with your team - , then deploy it across your organization by integrating it with your SSO and internal AI gateway. + and no licensing restrictions or ownership claims. Start a trial with your team, + then deploy it across your organization by integrating it with your SSO and + internal AI gateway.

Let us know and how we can help.

-
-
- - - +
+
+ + + +
+ Thanks to OpenCode, we found a way to create software to track all our assets — + even the imaginary ones. +
+ + + + + + + + + + + +
- Thanks to OpenCode, we found a way to create software to track all our assets — - even the imaginary ones. -
- - - - - - - - - - - -
-
diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index 287c2573..fe6c5698 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -219,8 +219,8 @@ export default function Home() {
[*]

- With over 26,000 GitHub stars, 188 contributors, and almost{" "} - 3,000 commits, OpenCode is used and trusted by over 200,000{" "} + With over 29,000 GitHub stars, 230 contributors, and almost{" "} + 3,500 commits, OpenCode is used and trusted by over 250,000{" "} developers every month.

@@ -274,7 +274,7 @@ export default function Home() { -
Fig 1.
26K GitHub Stars +
Fig 1.
29K GitHub Stars
@@ -577,7 +577,7 @@ export default function Home() { -
Fig 2.
188 Contributors +
Fig 2.
230 Contributors
@@ -619,7 +619,7 @@ export default function Home() { -
Fig 3.
200K Monthly Devs +
Fig 3.
250K Monthly Devs
diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index f7a1f0e1..3163de34 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -13,7 +13,11 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" import { logger } from "./logger" import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error" -import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider" +import { + createBodyConverter, + createStreamPartConverter, + createResponseConverter, +} from "./provider/provider" import { Format } from "./format" import { anthropicHelper } from "./provider/anthropic" import { openaiHelper } from "./provider/openai" @@ -43,7 +47,11 @@ export async function handler( }) const zenData = ZenData.list() const modelInfo = validateModel(zenData, body.model) - const providerInfo = selectProvider(zenData, modelInfo, input.request.headers.get("x-real-ip") ?? "") + const providerInfo = selectProvider( + zenData, + modelInfo, + input.request.headers.get("x-real-ip") ?? "", + ) const authInfo = await authenticate(modelInfo, providerInfo) validateBilling(modelInfo, authInfo) validateModelSettings(authInfo) @@ -222,7 +230,11 @@ export async function handler( return { id: modelId, ...modelData } } - function selectProvider(zenData: ZenData, model: Awaited>, ip: string) { + function selectProvider( + zenData: ZenData, + model: Awaited>, + ip: string, + ) { const providers = model.providers .filter((provider) => !provider.disabled) .flatMap((provider) => Array(provider.weight ?? 1).fill(provider)) @@ -239,7 +251,11 @@ export async function handler( return { ...provider, ...zenData.providers[provider.id], - ...(provider.id === "anthropic" ? anthropicHelper : provider.id === "openai" ? openaiHelper : oaCompatHelper), + ...(provider.id === "anthropic" + ? anthropicHelper + : provider.id === "openai" + ? openaiHelper + : oaCompatHelper), } } @@ -279,11 +295,20 @@ export async function handler( .from(KeyTable) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) - .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID))) - .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id))) + .innerJoin( + UserTable, + and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)), + ) + .leftJoin( + ModelTable, + and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)), + ) .leftJoin( ProviderTable, - and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)), + and( + eq(ProviderTable.workspaceID, KeyTable.workspaceID), + eq(ProviderTable.provider, providerInfo.id), + ), ) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), @@ -307,12 +332,20 @@ export async function handler( } function validateBilling(model: Model, authInfo: Awaited>) { - if (!authInfo || authInfo.isFree) return + if (!authInfo) return + if (authInfo.provider?.credentials) return + if (authInfo.isFree) return if (model.allowAnonymous) return const billing = authInfo.billing - if (!billing.paymentMethodID) throw new CreditsError("No payment method") - if (billing.balance <= 0) throw new CreditsError("Insufficient balance") + if (!billing.paymentMethodID) + throw new CreditsError( + `No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, + ) + if (billing.balance <= 0) + throw new CreditsError( + `Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, + ) const now = new Date() const currentYear = now.getUTCFullYear() @@ -327,7 +360,7 @@ export async function handler( const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth() if (currentYear === dateYear && currentMonth === dateMonth) throw new MonthlyLimitError( - `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}.`, + `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`, ) } @@ -340,7 +373,9 @@ export async function handler( const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth() if (currentYear === dateYear && currentMonth === dateMonth) - throw new UserLimitError(`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}.`) + throw new UserLimitError( + `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`, + ) } } @@ -364,12 +399,19 @@ export async function handler( providerInfo: Awaited>, usage: any, ) { - const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = - providerInfo.normalizeUsage(usage) + const { + inputTokens, + outputTokens, + reasoningTokens, + cacheReadTokens, + cacheWrite5mTokens, + cacheWrite1hTokens, + } = providerInfo.normalizeUsage(usage) const modelCost = modelInfo.cost200K && - inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000 + inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > + 200_000 ? modelInfo.cost200K : modelInfo.cost @@ -420,7 +462,8 @@ export async function handler( if (!authInfo) return - const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent) + const cost = + authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent) await Database.transaction(async (tx) => { await tx.insert(UsageTable).values({ workspaceID: authInfo.workspaceID, @@ -460,7 +503,9 @@ export async function handler( `, timeMonthlyUsageUpdated: sql`now()`, }) - .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))) + .where( + and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)), + ) }) await Database.use((tx) => @@ -487,7 +532,10 @@ export async function handler( eq(BillingTable.workspaceID, authInfo.workspaceID), eq(BillingTable.reload, true), lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)), - or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)), + or( + isNull(BillingTable.timeReloadLockedTill), + lt(BillingTable.timeReloadLockedTill, sql`now()`), + ), ), ), ) diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts new file mode 100644 index 00000000..af9bcc3a --- /dev/null +++ b/packages/console/core/script/lookup-user.ts @@ -0,0 +1,33 @@ +import { Database, eq } from "../src/drizzle/index.js" +import { AuthTable } from "../src/schema/auth.sql" + +// get input from command line +const email = process.argv[2] +if (!email) { + console.error("Usage: bun lookup-user.ts ") + process.exit(1) +} + +const authData = await printTable("Auth", (tx) => + tx.select().from(AuthTable).where(eq(AuthTable.subject, email)), +) +if (authData.length === 0) { + console.error("User not found") + process.exit(1) +} + +await printTable("Auth", (tx) => + tx.select().from(AuthTable).where(eq(AuthTable.accountID, authData[0].accountID)), +) + +function printTable( + title: string, + callback: (tx: Database.TxOrDb) => Promise, +): Promise { + return Database.use(async (tx) => { + const data = await callback(tx) + console.log(`== ${title} ==`) + console.table(data) + return data + }) +} diff --git a/packages/console/core/script/reset-db.ts b/packages/console/core/script/reset-db.ts index 96ecf14e..bd00e196 100644 --- a/packages/console/core/script/reset-db.ts +++ b/packages/console/core/script/reset-db.ts @@ -1,13 +1,21 @@ import { Resource } from "@opencode-ai/console-resource" -import { Database } from "@opencode-ai/console-core/drizzle/index.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { AccountTable } from "@opencode-ai/console-core/schema/account.sql.js" -import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" -import { BillingTable, PaymentTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { Database } from "../src/drizzle/index.js" +import { UserTable } from "../src/schema/user.sql.js" +import { AccountTable } from "../src/schema/account.sql.js" +import { WorkspaceTable } from "../src/schema/workspace.sql.js" +import { BillingTable, PaymentTable, UsageTable } from "../src/schema/billing.sql.js" +import { KeyTable } from "../src/schema/key.sql.js" if (Resource.App.stage !== "frank") throw new Error("This script is only for frank") -for (const table of [AccountTable, BillingTable, KeyTable, PaymentTable, UsageTable, UserTable, WorkspaceTable]) { +for (const table of [ + AccountTable, + BillingTable, + KeyTable, + PaymentTable, + UsageTable, + UserTable, + WorkspaceTable, +]) { await Database.use((tx) => tx.delete(table)) } diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 40081521..32fe27b8 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -4,6 +4,7 @@ "description": "", "type": "module", "scripts": { + "typecheck": "tsgo --noEmit", "start": "vite", "dev": "vite", "build": "vite build", @@ -11,7 +12,6 @@ }, "license": "MIT", "devDependencies": { - "opencode": "workspace:*", "@tailwindcss/vite": "catalog:", "@tsconfig/bun": "1.0.9", "@types/luxon": "3.7.1", @@ -26,7 +26,6 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@pierre/precision-diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/desktop/src/components/code.tsx b/packages/desktop/src/components/code.tsx index 11518e73..c214fd5e 100644 --- a/packages/desktop/src/components/code.tsx +++ b/packages/desktop/src/components/code.tsx @@ -2,7 +2,7 @@ import { bundledLanguages, type BundledLanguage, type ShikiTransformer } from "s import { splitProps, type ComponentProps, createEffect, onMount, onCleanup, createMemo, createResource } from "solid-js" import { useLocal, type TextSelection } from "@/context/local" import { getFileExtension, getNodeOffsetInLine, getSelectionInContainer } from "@/utils" -import { useShiki } from "@/context/shiki" +import { useShiki } from "@opencode-ai/ui" type DefinedSelection = Exclude diff --git a/packages/desktop/src/components/diff-changes.tsx b/packages/desktop/src/components/diff-changes.tsx deleted file mode 100644 index 3b633f70..00000000 --- a/packages/desktop/src/components/diff-changes.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { FileDiff } from "@opencode-ai/sdk" -import { createMemo, Show } from "solid-js" - -export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) { - const additions = createMemo(() => - Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) : props.diff.additions, - ) - const deletions = createMemo(() => - Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) : props.diff.deletions, - ) - const total = createMemo(() => additions() + deletions()) - return ( - 0}> -
- {`+${additions()}`} - {`-${deletions()}`} -
-
- ) -} diff --git a/packages/desktop/src/components/markdown.tsx b/packages/desktop/src/components/markdown.tsx deleted file mode 100644 index e0f185f5..00000000 --- a/packages/desktop/src/components/markdown.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useMarked } from "@/context/marked" -import { createResource } from "solid-js" - -function strip(text: string): string { - const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ - const match = text.match(wrappedRe) - return match ? match[2] : text -} -export function Markdown(props: { text: string; class?: string }) { - const marked = useMarked() - const [html] = createResource( - () => strip(props.text), - async (markdown) => { - return marked.parse(markdown) - }, - ) - return ( -
- ) -} diff --git a/packages/desktop/src/components/message.tsx b/packages/desktop/src/components/message.tsx deleted file mode 100644 index 589ca311..00000000 --- a/packages/desktop/src/components/message.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import type { Part, ReasoningPart, TextPart, ToolPart, Message, AssistantMessage, UserMessage } from "@opencode-ai/sdk" -import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js" -import { Dynamic } from "solid-js/web" -import { Markdown } from "./markdown" -import { Checkbox, Collapsible, Diff, Icon, IconProps } from "@opencode-ai/ui" -import { getDirectory, getFilename } from "@/utils" -import type { Tool } from "opencode/tool/tool" -import type { ReadTool } from "opencode/tool/read" -import type { ListTool } from "opencode/tool/ls" -import type { GlobTool } from "opencode/tool/glob" -import type { GrepTool } from "opencode/tool/grep" -import type { WebFetchTool } from "opencode/tool/webfetch" -import type { TaskTool } from "opencode/tool/task" -import type { BashTool } from "opencode/tool/bash" -import type { EditTool } from "opencode/tool/edit" -import type { WriteTool } from "opencode/tool/write" -import type { TodoWriteTool } from "opencode/tool/todo" -import { DiffChanges } from "./diff-changes" - -export function Message(props: { message: Message; parts: Part[] }) { - return ( - - - {(userMessage) => } - - - {(assistantMessage) => } - - - ) -} - -function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) { - const filteredParts = createMemo(() => { - return props.parts?.filter((x) => { - if (x.type === "reasoning") return false - return x.type !== "tool" || x.tool !== "todoread" - }) - }) - return ( -
- {(part) => } -
- ) -} - -function UserMessage(props: { message: UserMessage; parts: Part[] }) { - const text = createMemo(() => - props.parts - ?.filter((p) => p.type === "text" && !p.synthetic) - ?.map((p) => (p as TextPart).text) - ?.join(""), - ) - return
{text()}
-} - -export function Part(props: { part: Part; message: Message; hideDetails?: boolean }) { - const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING]) - return ( - - - - ) -} - -const PART_MAPPING = { - text: TextPart, - tool: ToolPart, - reasoning: ReasoningPart, -} - -function ReasoningPart(props: { part: ReasoningPart; message: Message }) { - return ( - - - - ) -} - -function TextPart(props: { part: TextPart; message: Message }) { - return ( - - - - ) -} - -function ToolPart(props: { part: ToolPart; message: Message; hideDetails?: boolean }) { - const component = createMemo(() => { - const render = ToolRegistry.render(props.part.tool) ?? GenericTool - const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {}) - const input = props.part.state.status === "completed" ? props.part.state.input : {} - - return ( - - ) - }) - - return {component()} -} - -type TriggerTitle = { - title: string - titleClass?: string - subtitle?: string - subtitleClass?: string - args?: string[] - argsClass?: string - action?: JSX.Element -} - -const isTriggerTitle = (val: any): val is TriggerTitle => { - return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node) -} - -function BasicTool(props: { - icon: IconProps["name"] - trigger: TriggerTitle | JSX.Element - children?: JSX.Element - hideDetails?: boolean -}) { - const resolved = children(() => props.children) - return ( - - -
-
- -
- - - {(trigger) => ( -
-
- - {trigger().title} - - - - {trigger().subtitle} - - - - - {(arg) => ( - - {arg} - - )} - - -
- {trigger().action} -
- )} -
- {props.trigger as JSX.Element} -
-
-
- - - -
-
- - {resolved()} - -
- // <> - // {props.part.state.error.replace("Error: ", "")} - // - ) -} - -function GenericTool(props: ToolProps) { - return -} - -type ToolProps = { - input: Partial> - metadata: Partial> - tool: string - output?: string - hideDetails?: boolean -} - -const ToolRegistry = (() => { - const state: Record< - string, - { - name: string - render?: Component> - } - > = {} - function register(input: { name: string; render?: Component> }) { - state[input.name] = input - return input - } - return { - register, - render(name: string) { - return state[name]?.render - }, - } -})() - -ToolRegistry.register({ - name: "read", - render(props) { - return ( - - ) - }, -}) - -ToolRegistry.register({ - name: "list", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "glob", - render(props) { - return ( - {props.output}
-
- - ) - }, -}) - -ToolRegistry.register({ - name: "grep", - render(props) { - const args = [] - if (props.input.pattern) args.push("pattern=" + props.input.pattern) - if (props.input.include) args.push("include=" + props.input.include) - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "webfetch", - render(props) { - return ( - - - - ), - }} - > - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "task", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "bash", - render(props) { - return ( - - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "edit", - render(props) { - return ( - -
-
Edit
-
- - {getDirectory(props.input.filePath!)} - - {getFilename(props.input.filePath ?? "")} -
-
-
- - - -
- - } - > - -
- -
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "write", - render(props) { - return ( - -
-
Write
-
- - {getDirectory(props.input.filePath!)} - - {getFilename(props.input.filePath ?? "")} -
-
-
{/* */}
- - } - > - -
{props.output}
-
-
- ) - }, -}) - -ToolRegistry.register({ - name: "todowrite", - render(props) { - return ( - t.status === "completed").length}/${props.input.todos?.length}`, - }} - > - -
- - {(todo) => ( - -
{todo.content}
-
- )} -
-
-
-
- ) - }, -}) diff --git a/packages/desktop/src/components/progress-circle.tsx b/packages/desktop/src/components/progress-circle.tsx deleted file mode 100644 index d56197ed..00000000 --- a/packages/desktop/src/components/progress-circle.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, createMemo } from "solid-js" - -interface ProgressCircleProps { - percentage: number - size?: number - strokeWidth?: number -} - -export const ProgressCircle: Component = (props) => { - // --- Set default values for props --- - const size = () => props.size || 16 - const strokeWidth = () => props.strokeWidth || 3 - - // --- Constants for SVG calculation --- - const viewBoxSize = 16 - const center = viewBoxSize / 2 - const radius = () => center - strokeWidth() / 2 - const circumference = createMemo(() => 2 * Math.PI * radius()) - - // --- Reactive Calculation for the progress offset --- - const offset = createMemo(() => { - const clampedPercentage = Math.max(0, Math.min(100, props.percentage || 0)) - const progress = clampedPercentage / 100 - return circumference() * (1 - progress) - }) - - return ( - - - - - ) -} diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index d6276c15..f1c19388 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -71,7 +71,7 @@ export const PromptInput: Component = (props) => { } }) - const { flat, active, onInput, onKeyDown } = useFilteredList({ + const { flat, active, onInput, onKeyDown, refetch } = useFilteredList({ items: local.file.search, key: (x) => x, onSelect: (path) => { @@ -81,6 +81,11 @@ export const PromptInput: Component = (props) => { }, }) + createEffect(() => { + local.model.recent() + refetch() + }) + createEffect( on( () => store.contentParts, @@ -369,16 +374,20 @@ export const PromptInput: Component = (props) => { items={local.model.list()} current={local.model.current()} filterKeys={["provider.name", "name", "id"]} - groupBy={(x) => x.provider.name} + groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)} sortGroupsBy={(a, b) => { const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] + if (a.category === "Recent" && b.category !== "Recent") return -1 + if (b.category === "Recent" && a.category !== "Recent") return 1 const aProvider = a.items[0].provider.id const bProvider = b.items[0].provider.id if (order.includes(aProvider) && !order.includes(bProvider)) return -1 if (!order.includes(aProvider) && order.includes(bProvider)) return 1 return order.indexOf(aProvider) - order.indexOf(bProvider) }} - onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)} + onSelect={(x) => + local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true }) + } trigger={