mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into opentui
This commit is contained in:
commit
b6d4e1acaf
41 changed files with 646 additions and 320 deletions
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
|
@ -24,6 +24,7 @@ jobs:
|
|||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
env:
|
||||
CI: true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
description: Git commit and push
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
make sure it includes a prefix like
|
||||
|
|
@ -8,6 +12,10 @@ ci:
|
|||
ignore:
|
||||
wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
|
|
|
|||
26
bun.lock
26
bun.lock
|
|
@ -33,6 +33,7 @@
|
|||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.0",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "0.30.5",
|
||||
"mysql2": "3.14.4",
|
||||
"typescript": "catalog:",
|
||||
|
|
@ -81,6 +83,7 @@
|
|||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
|
|
@ -147,6 +150,7 @@
|
|||
"@tsconfig/bun": "1.0.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
|
|
@ -181,6 +185,7 @@
|
|||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.0.0-20251010-2eed09fd",
|
||||
"@opentui/solid": "0.0.0-20251010-2eed09fd",
|
||||
|
|
@ -229,6 +234,7 @@
|
|||
"@types/bun": "catalog:",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
|
|
@ -245,6 +251,7 @@
|
|||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
|
|
@ -261,6 +268,7 @@
|
|||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
|
|
@ -273,6 +281,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
|
|
@ -345,6 +354,7 @@
|
|||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.0",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||
"ai": "5.0.8",
|
||||
"diff": "8.0.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
|
|
@ -1420,6 +1430,22 @@
|
|||
|
||||
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251014.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251014.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IqmX5CYCBqXbfL+HKlcQAMaDlfJ0Z8OhUxvADFV2TENnzSYI4CuhvKxwOB2wFSLXufVsgtAlf3Fjwn24KmMyPQ=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7rQoLlerWnwnvrM56hP4rdEbo4xDE4zr7cch+EzgENq/tbXYereGq1fmnR83UNglb1Eyy53OvJZ3O2csYBa2vg=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-SF29o9NFRGDM23Jz0nVO4/yS78GQ81rtOemmCVNXuJotoY4bP3npGDyEmfkZQHZgDOXogs2OWy3t7NUJ235ANQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm" }, "sha512-o5cu7h+BBAp6V4qxYY5RWuaYouN3j+MGFLrrUtvvNj4XKM+kbq5qwsgVRsmJZ1LfUvHmzyQs86vt9djAWedzjQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YWbW/JF4uggEUBr+vflqI5i7bL4Z3XInCOyUO1qQEY7VmfDCsPEzIwGi37O1mixfxw9Qj8LQsptCkU+fqKwGw=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3LC4tgcgi6zWJWBUpBNXOGSY3yISJrQezSP/T+v+mQRApkdoIpTSHIyQAhgaagcs3MOQRaqiIPaLOVrdHXdU6A=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-P0D4UEXwzFZh3pHexe2Ky1tW/HjY/HxTBTIajz2ViDCNPw7uDSEsXSB4H9TTiFJw8gVdTUFbsoAQp1MteTeORA=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fi53g2ihH7tkQLlz8hZGAb2V+3aNZpcxrZ530CQ4xcWwAqssEj0EaZJX0VLEtIQBar1ttGVK9Pz/wJU9sYyVzg=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vercel/nft": ["@vercel/nft@0.30.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-pquXF3XZFg/T3TBor08rUhIGgOhdSilbn7WQLVP/aVSSO+25Rs4H/m3nxNDQ2x3znX7Z3yYjryN8xaLwypcwQg=="],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"typescript": "5.8.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"solid-js": "1.9.9",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@opencode-ai/console-app",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vinxi dev --host 0.0.0.0",
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
|
|
@ -25,7 +25,8 @@
|
|||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { A, createAsync } from "@solidjs/router"
|
|||
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { github } from "~/lib/github"
|
||||
import { queryIsLoggedIn } from "~/routes/workspace/common"
|
||||
|
||||
export function Header(props: { zen?: boolean }) {
|
||||
const githubData = createAsync(() => github())
|
||||
const isLoggedIn = createAsync(() => queryIsLoggedIn())
|
||||
const starCount = createMemo(() =>
|
||||
githubData()?.stars
|
||||
? new Intl.NumberFormat("en-US", {
|
||||
|
|
@ -39,7 +41,7 @@ export function Header(props: { zen?: boolean }) {
|
|||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">Login</a>
|
||||
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href="/zen">Zen</A>
|
||||
|
|
@ -110,7 +112,7 @@ export function Header(props: { zen?: boolean }) {
|
|||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">Login</a>
|
||||
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href="/zen">Zen</A>
|
||||
|
|
|
|||
|
|
@ -118,3 +118,86 @@ export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
d="M8.43799 8.06943V6.09387C8.43799 5.92749 8.50347 5.80267 8.65601 5.71959L12.8206 3.43211C13.3875 3.1202 14.0635 2.9747 14.7611 2.9747C17.3775 2.9747 19.0347 4.9087 19.0347 6.96734C19.0347 7.11288 19.0347 7.27926 19.0128 7.44564L14.6956 5.03335C14.434 4.88785 14.1723 4.88785 13.9107 5.03335L8.43799 8.06943ZM18.1624 15.7637V11.0431C18.1624 10.7519 18.0315 10.544 17.7699 10.3984L12.2972 7.36234L14.0851 6.3849C14.2377 6.30182 14.3686 6.30182 14.5212 6.3849L18.6858 8.67238C19.8851 9.3379 20.6917 10.7519 20.6917 12.1243C20.6917 13.7047 19.7106 15.1604 18.1624 15.7636V15.7637ZM7.15158 11.6047L5.36369 10.6066C5.21114 10.5235 5.14566 10.3986 5.14566 10.2323V5.65735C5.14566 3.43233 6.93355 1.7478 9.35381 1.7478C10.2697 1.7478 11.1199 2.039 11.8396 2.55886L7.54424 4.92959C7.28268 5.07508 7.15181 5.28303 7.15181 5.57427V11.6049L7.15158 11.6047ZM11 13.7258L8.43799 12.3533V9.44209L11 8.06965L13.5618 9.44209V12.3533L11 13.7258ZM12.6461 20.0476C11.7303 20.0476 10.8801 19.7564 10.1604 19.2366L14.4557 16.8658C14.7173 16.7203 14.8482 16.5124 14.8482 16.2211V10.1905L16.658 11.1886C16.8105 11.2717 16.876 11.3965 16.876 11.563V16.1379C16.876 18.3629 15.0662 20.0474 12.6461 20.0474V20.0476ZM7.47863 15.4103L3.314 13.1229C2.11471 12.4573 1.30808 11.0433 1.30808 9.67088C1.30808 8.06965 2.31106 6.6348 3.85903 6.03168V10.773C3.85903 11.0642 3.98995 11.2721 4.25151 11.4177L9.70253 14.4328L7.91464 15.4103C7.76209 15.4934 7.63117 15.4934 7.47863 15.4103ZM7.23892 18.8207C4.77508 18.8207 2.96533 17.0531 2.96533 14.8696C2.96533 14.7032 2.98719 14.5368 3.00886 14.3704L7.30418 16.7412C7.56574 16.8867 7.82752 16.8867 8.08909 16.7412L13.5618 13.726V15.7015C13.5618 15.8679 13.4964 15.9927 13.3438 16.0758L9.17918 18.3633C8.61225 18.6752 7.93631 18.8207 7.23869 18.8207H7.23892ZM12.6461 21.2952C15.2844 21.2952 17.4865 19.5069 17.9882 17.1362C20.4301 16.5331 22 14.3495 22 12.1245C22 10.6688 21.346 9.25482 20.1685 8.23581C20.2775 7.79908 20.343 7.36234 20.343 6.92582C20.343 3.95215 17.8137 1.72691 14.892 1.72691C14.3034 1.72691 13.7365 1.80999 13.1695 1.99726C12.1882 1.08223 10.8364 0.5 9.35381 0.5C6.71557 0.5 4.51352 2.28829 4.01185 4.65902C1.56987 5.26214 0 7.44564 0 9.67067C0 11.1264 0.654039 12.5404 1.83147 13.5594C1.72246 13.9961 1.65702 14.4328 1.65702 14.8694C1.65702 17.8431 4.1863 20.0683 7.108 20.0683C7.69661 20.0683 8.26354 19.9852 8.83046 19.7979C9.81155 20.713 11.1634 21.2952 12.6461 21.2952Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconXai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconAlibaba(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M11.6043 0.340162C11.9973 1.03016 12.3883 1.72215 12.7783 2.41514C12.7941 2.44286 12.8169 2.46589 12.8445 2.48187C12.8721 2.49786 12.9034 2.50624 12.9353 2.50614H18.4873C18.6612 2.50614 18.8092 2.61614 18.9332 2.83314L20.3872 5.40311C20.5772 5.74011 20.6272 5.88111 20.4112 6.24011C20.1512 6.6701 19.8982 7.1041 19.6512 7.54009L19.2842 8.19809C19.1782 8.39409 19.0612 8.47809 19.2442 8.71008L21.8962 13.347C22.0682 13.648 22.0072 13.841 21.8532 14.117C21.4162 14.902 20.9712 15.681 20.5182 16.457C20.3592 16.729 20.1662 16.832 19.8382 16.827C19.0612 16.811 18.2863 16.817 17.5113 16.843C17.4946 16.8439 17.4785 16.8489 17.4644 16.8576C17.4502 16.8664 17.4385 16.8785 17.4303 16.893C16.5361 18.4773 15.6344 20.0573 14.7253 21.633C14.5563 21.926 14.3453 21.996 14.0003 21.997C13.0033 22 11.9983 22.001 10.9833 21.999C10.8889 21.9987 10.7961 21.9735 10.7145 21.9259C10.6328 21.8783 10.5652 21.8101 10.5184 21.728L9.18337 19.405C9.1756 19.3898 9.16368 19.3771 9.14898 19.3684C9.13429 19.3598 9.11743 19.3554 9.10037 19.356H3.98244C3.69744 19.386 3.42944 19.355 3.17745 19.264L1.57447 16.494C1.52706 16.412 1.50193 16.319 1.50158 16.2243C1.50123 16.1296 1.52567 16.0364 1.57247 15.954L2.77945 13.834C2.79665 13.8041 2.80569 13.7701 2.80569 13.7355C2.80569 13.701 2.79665 13.667 2.77945 13.637C2.15073 12.5485 1.52573 11.4579 0.904476 10.3651L0.114486 8.97008C-0.0455115 8.66008 -0.0585113 8.47409 0.209485 8.00509C0.674479 7.1921 1.13647 6.38011 1.59647 5.56911C1.72847 5.33512 1.90046 5.23512 2.18046 5.23412C3.04344 5.23048 3.90644 5.23015 4.76943 5.23312C4.79123 5.23295 4.81259 5.22704 4.83138 5.21597C4.85016 5.20491 4.8657 5.1891 4.87643 5.17012L7.68239 0.275163C7.72491 0.200697 7.78631 0.138751 7.86039 0.0955646C7.93448 0.0523783 8.01863 0.0294762 8.10439 0.0291651C8.62838 0.0281651 9.15737 0.029165 9.68736 0.0231651L10.7044 0.000165317C11.0453 -0.00283466 11.4283 0.032165 11.6043 0.340162ZM8.17238 0.743158C8.16185 0.743152 8.15149 0.745921 8.14236 0.751187C8.13323 0.756454 8.12565 0.764031 8.12038 0.773158L5.25442 5.78811C5.24066 5.81174 5.22097 5.83137 5.19729 5.84505C5.17361 5.85873 5.14677 5.86599 5.11942 5.86611H2.25346C2.19746 5.86611 2.18346 5.89111 2.21246 5.94011L8.02239 16.096C8.04739 16.138 8.03539 16.158 7.98839 16.159L5.19342 16.174C5.15256 16.1727 5.11214 16.1828 5.07678 16.2033C5.04141 16.2238 5.01253 16.2539 4.99342 16.29L3.67344 18.6C3.62944 18.678 3.65244 18.718 3.74144 18.718L9.45737 18.726C9.50337 18.726 9.53737 18.746 9.56137 18.787L10.9643 21.241C11.0103 21.322 11.0563 21.323 11.1033 21.241L16.1093 12.481L16.8923 11.0991C16.897 11.0905 16.904 11.0834 16.9125 11.0785C16.9209 11.0735 16.9305 11.0709 16.9403 11.0709C16.9501 11.0709 16.9597 11.0735 16.9681 11.0785C16.9765 11.0834 16.9835 11.0905 16.9883 11.0991L18.4123 13.629C18.4229 13.648 18.4385 13.6637 18.4573 13.6746C18.4761 13.6855 18.4975 13.6912 18.5193 13.691L21.2822 13.671C21.2893 13.6711 21.2963 13.6693 21.3024 13.6658C21.3086 13.6623 21.3137 13.6572 21.3172 13.651C21.3206 13.6449 21.3224 13.638 21.3224 13.631C21.3224 13.624 21.3206 13.6172 21.3172 13.611L18.4173 8.52508C18.4068 8.50809 18.4013 8.48853 18.4013 8.46859C18.4013 8.44864 18.4068 8.42908 18.4173 8.41209L18.7102 7.90509L19.8302 5.92811C19.8542 5.88711 19.8422 5.86611 19.7952 5.86611H8.20038C8.14138 5.86611 8.12738 5.84011 8.15738 5.78911L9.59137 3.28413C9.60211 3.26706 9.60781 3.24731 9.60781 3.22714C9.60781 3.20697 9.60211 3.18721 9.59137 3.17014L8.22538 0.774158C8.22016 0.764697 8.21248 0.756822 8.20315 0.751365C8.19382 0.745909 8.18319 0.743073 8.17238 0.743158ZM14.4623 8.76308C14.5083 8.76308 14.5203 8.78308 14.4963 8.82308L13.6643 10.2881L11.0513 14.873C11.0464 14.8819 11.0392 14.8894 11.0304 14.8945C11.0216 14.8996 11.0115 14.9022 11.0013 14.902C10.9912 14.902 10.9813 14.8993 10.9725 14.8942C10.9637 14.8891 10.9564 14.8818 10.9513 14.873L7.49839 8.84108C7.47839 8.80708 7.48839 8.78908 7.52639 8.78708L7.74239 8.77508L14.4643 8.76308H14.4623Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMoonshotAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20.052 6.0364C18.7527 4.28846 16.8758 2.94985 14.6066 2.34296C12.3374 1.73606 10.0411 1.95816 8.04092 2.82331L20.052 6.0364ZM3.75774 6.34071C4.64576 5.04902 5.81872 3.99836 7.16325 3.25459L16.0115 5.62191C15.4308 5.95635 14.8088 6.53268 14.3273 7.16626L21.6025 9.11263C21.809 9.79398 21.9435 10.5003 22 11.2213L3.75774 6.34071ZM21.6866 14.5876C21.584 14.9707 21.4603 15.3425 21.3172 15.7019L2.10543 10.5623C2.16147 10.1792 2.24079 9.7957 2.34339 9.41263C2.58479 8.51262 2.94172 7.67459 3.39435 6.91016L12.7957 9.42554C12.4194 9.96271 12.0766 10.5464 11.7749 11.1709L21.8517 13.8671C21.8056 14.1077 21.7504 14.3478 21.6862 14.5884L21.6866 14.5876ZM2.58134 15.3529C2.11535 14.05 1.91662 12.6408 2.03215 11.2088L10.8985 13.5808C10.8347 13.7823 10.7748 13.9863 10.7192 14.1933C10.6062 14.6147 10.514 15.0352 10.4424 15.4527L20.0166 18.0142C19.5709 18.6051 19.0631 19.1406 18.5057 19.6132L2.58134 15.3529ZM9.42338 21.6568C6.40111 20.8481 4.07415 18.7424 2.88266 16.1001L10.0976 18.0305C10.0859 18.7024 10.1278 19.3571 10.2196 19.9851L15.4619 21.3874C13.5906 22.0743 11.496 22.2112 9.42338 21.6568Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
||||
<path
|
||||
d="M24 15.5L18.1816 14.2871L18.1328 14.3115C16.9036 11.7879 15.6135 9.29301 14.2607 6.82812C14.0172 6.38435 13.771 5.94188 13.5234 5.5C13.6911 5.97998 13.8606 6.45942 14.0322 6.9375C14.9902 9.60529 16.012 12.2429 17.0947 14.8516L12 17.5L6.9043 14.8516C7.98712 12.2428 9.00977 9.6054 9.96777 6.9375C10.1394 6.45942 10.3089 5.97998 10.4766 5.5C10.229 5.94188 9.98281 6.38435 9.73926 6.82812C8.38629 9.29339 7.09557 11.7884 5.86621 14.3125L5.81738 14.2871L0 15.5L12 0.5L24 15.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function Home() {
|
|||
<main data-page="opencode">
|
||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
||||
<Title>OpenCode | The AI coding agent built for the terminal</Title>
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<Meta property="og:image" content="/social-share.png" />
|
||||
<Meta name="twitter:image" content="/social-share.png" />
|
||||
<div data-component="container">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { UserMenu } from "./user-menu"
|
|||
import { withActor } from "~/context/auth.withActor"
|
||||
import { User } from "@opencode-ai/console-core/user.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { Link } from "@solidjs/meta"
|
||||
|
||||
const getUserEmail = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
|
|
@ -21,6 +22,7 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||
const userEmail = createAsync(() => getUserEmail(params.id))
|
||||
return (
|
||||
<main data-page="workspace">
|
||||
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
|
||||
<header data-component="workspace-header">
|
||||
<div data-slot="header-brand">
|
||||
<A href="/" data-component="site-title">
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export function PaymentSection() {
|
|||
}}
|
||||
data-slot="receipt-button"
|
||||
>
|
||||
view
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function () {
|
|||
}
|
||||
>
|
||||
<span data-slot="balance">
|
||||
Current balance: <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
|
||||
Current balance <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@
|
|||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-slot="training-data"] {
|
||||
|
|
@ -88,8 +94,8 @@
|
|||
}
|
||||
|
||||
/* Checked state - track */
|
||||
input:checked+span {
|
||||
background-color: #21AD0E;
|
||||
input:checked + span {
|
||||
background-color: #21ad0e;
|
||||
border-color: #148605;
|
||||
|
||||
/* Checked state - handle */
|
||||
|
|
@ -103,7 +109,7 @@
|
|||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
input:checked:hover+span {
|
||||
input:checked:hover + span {
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
|
|
@ -112,16 +118,16 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:disabled+span {
|
||||
input:disabled + span {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:disabled:checked+span {
|
||||
input:disabled:checked + span {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input:disabled~span:hover {
|
||||
input:disabled ~ span:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +148,6 @@
|
|||
|
||||
@media (max-width: 40rem) {
|
||||
[data-slot="models-table-element"] {
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
|
|
@ -152,8 +157,7 @@
|
|||
th {
|
||||
&:nth-child(2)
|
||||
|
||||
/* Training Data */
|
||||
{
|
||||
/* Training Data */ {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -161,10 +165,9 @@
|
|||
td {
|
||||
&:nth-child(2)
|
||||
|
||||
/* Training Data */
|
||||
{
|
||||
/* Training Data */ {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ import { withActor } from "~/context/auth.withActor"
|
|||
import { ZenModel } from "@opencode-ai/console-core/model.js"
|
||||
import styles from "./model-section.module.css"
|
||||
import { querySessionInfo } from "../common"
|
||||
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
|
||||
|
||||
const getModelLab = (modelId: string) => {
|
||||
if (modelId.startsWith("claude")) return "Anthropic"
|
||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||
if (modelId.startsWith("glm")) return "Z.ai"
|
||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||
if (modelId.startsWith("grok")) return "xAI"
|
||||
return "Stealth"
|
||||
}
|
||||
|
||||
const getModelsInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
|
|
@ -12,6 +23,7 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||
return {
|
||||
all: Object.entries(ZenModel.list())
|
||||
.filter(([id, _model]) => !["claude-3-5-haiku", "qwen3-max"].includes(id))
|
||||
.filter(([id, _model]) => !id.startsWith("an-"))
|
||||
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
|
||||
.map(([id, model]) => ({ id, name: model.name })),
|
||||
disabled: await Model.listDisabled(),
|
||||
|
|
@ -42,13 +54,21 @@ export function ModelSection() {
|
|||
const params = useParams()
|
||||
const modelsInfo = createAsync(() => getModelsInfo(params.id))
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id))
|
||||
|
||||
const modelsWithLab = createMemo(() => {
|
||||
const info = modelsInfo()
|
||||
if (!info) return []
|
||||
return info.all.map((model) => ({
|
||||
...model,
|
||||
lab: getModelLab(model.id),
|
||||
}))
|
||||
})
|
||||
return (
|
||||
<section class={styles.root}>
|
||||
<div data-slot="section-title">
|
||||
<h2>Models</h2>
|
||||
<p>
|
||||
Manage which models workspace members can access. Requests will fail if a member tries to use a disabled
|
||||
model.{userInfo()?.isAdmin ? "" : " To use a disabled model, contact your workspace’s admin."}
|
||||
Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div data-slot="models-list">
|
||||
|
|
@ -58,16 +78,40 @@ export function ModelSection() {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th></th>
|
||||
<th>Enabled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={modelsInfo()!.all}>
|
||||
{({ id, name }) => {
|
||||
<For each={modelsWithLab()}>
|
||||
{({ id, name, lab }) => {
|
||||
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
|
||||
return (
|
||||
<tr data-slot="model-row" data-disabled={!isEnabled()}>
|
||||
<td data-slot="model-name">{name}</td>
|
||||
<td data-slot="model-name">
|
||||
<div>
|
||||
{(() => {
|
||||
switch (lab) {
|
||||
case "OpenAI":
|
||||
return <IconOpenAI width={16} height={16} />
|
||||
case "Anthropic":
|
||||
return <IconAnthropic width={16} height={16} />
|
||||
case "Moonshot AI":
|
||||
return <IconMoonshotAI width={16} height={16} />
|
||||
case "Z.ai":
|
||||
return <IconZai width={16} height={16} />
|
||||
case "Alibaba":
|
||||
return <IconAlibaba width={16} height={16} />
|
||||
case "xAI":
|
||||
return <IconXai width={16} height={16} />
|
||||
default:
|
||||
return <IconStealth width={16} height={16} />
|
||||
}
|
||||
})()}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-slot="model-lab">{lab}</td>
|
||||
<td data-slot="model-toggle">
|
||||
<form action={updateModel} method="post">
|
||||
<input type="hidden" name="model" value={id} />
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@ export function formatDateUTC(date: Date) {
|
|||
return date.toLocaleDateString("en-US", options)
|
||||
}
|
||||
|
||||
export const queryIsLoggedIn = query(async () => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
try {
|
||||
Actor.assert("account")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}, "isLoggedIn.get")
|
||||
|
||||
export const querySessionInfo = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import "./index.css"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { HttpHeader } from "@solidjs/start"
|
||||
import zenLogoLight from "../../asset/zen-ornate-light.svg"
|
||||
|
|
@ -15,8 +16,10 @@ import { Faq } from "~/component/faq"
|
|||
import { Legal } from "~/component/legal"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Header } from "~/component/header"
|
||||
import { queryIsLoggedIn } from "~/routes/workspace/common"
|
||||
|
||||
export default function Home() {
|
||||
const isLoggedIn = createAsync(() => queryIsLoggedIn())
|
||||
return (
|
||||
<main data-page="zen">
|
||||
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
|
||||
|
|
@ -102,7 +105,7 @@ export default function Home() {
|
|||
</div>
|
||||
</div>
|
||||
<a href="/auth">
|
||||
<span>Get started with Zen </span>
|
||||
<span>{isLoggedIn() ? "Go to workspace " : "Get started with Zen "}</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"update-models": "script/update-models.ts",
|
||||
"promote-models-to-dev": "script/promote-models.ts dev",
|
||||
"promote-models-to-prod": "script/promote-models.ts production",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
"@types/node": "catalog:",
|
||||
"drizzle-kit": "0.30.5",
|
||||
"mysql2": "3.14.4",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export namespace User {
|
|||
const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
|
||||
await AWS.sendEmail({
|
||||
to: email,
|
||||
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode Console`,
|
||||
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode`,
|
||||
body: render(
|
||||
// @ts-ignore
|
||||
InviteEmail({
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/node": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
|
|
|
|||
|
|
@ -120,6 +120,10 @@ export default {
|
|||
|
||||
if (!email) throw new Error("No email found")
|
||||
|
||||
if (Resource.App.stage !== "production" && !email.endsWith("@anoma.ly")) {
|
||||
throw new Error("Invalid email")
|
||||
}
|
||||
|
||||
let accountID = await Account.fromEmail(email).then((x) => x?.id)
|
||||
if (!accountID) {
|
||||
console.log("creating account for", email)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,10 @@
|
|||
// @ts-nocheck
|
||||
import React from "react"
|
||||
import { Font, Hr as JEHr, Text as JEText, type HrProps, type TextProps } from "@jsx-email/all"
|
||||
import { DIVIDER_COLOR, SURFACE_DIVIDER_COLOR, textColor } from "./styles"
|
||||
import { Font, Text as JEText, type TextProps } from "@jsx-email/all"
|
||||
import { baseText } from "./styles"
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
return <JEText {...props} style={{ ...textColor, ...props.style }} />
|
||||
}
|
||||
|
||||
export function Hr(props: HrProps) {
|
||||
return <JEHr {...props} style={{ borderTop: `1px solid ${DIVIDER_COLOR}`, ...props.style }} />
|
||||
}
|
||||
|
||||
export function SurfaceHr(props: HrProps) {
|
||||
return (
|
||||
<JEHr
|
||||
{...props}
|
||||
style={{
|
||||
borderTop: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <JEText {...props} style={{ ...baseText, ...props.style }} />
|
||||
}
|
||||
|
||||
export function Title({ children }: TitleProps) {
|
||||
|
|
@ -31,10 +15,6 @@ export function A({ children, ...props }: AProps) {
|
|||
return React.createElement("a", props, children)
|
||||
}
|
||||
|
||||
export function B({ children, ...props }: AProps) {
|
||||
return React.createElement("b", props, children)
|
||||
}
|
||||
|
||||
export function Span({ children, ...props }: SpanProps) {
|
||||
return React.createElement("span", props, children)
|
||||
}
|
||||
|
|
@ -47,45 +27,25 @@ export function Fonts({ assetsUrl }: { assetsUrl: string }) {
|
|||
return (
|
||||
<>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fontFamily="JetBrains Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-400.woff2`,
|
||||
url: `${assetsUrl}/JetBrainsMono-Regular.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="400"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fontFamily="JetBrains Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-500.woff2`,
|
||||
url: `${assetsUrl}/JetBrainsMono-Medium.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="500"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-600.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="600"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="IBM Plex Mono"
|
||||
fallbackFontFamily="monospace"
|
||||
webFont={{
|
||||
url: `${assetsUrl}/ibm-plex-mono-latin-700.woff2`,
|
||||
format: "woff2",
|
||||
}}
|
||||
fontWeight="700"
|
||||
fontStyle="normal"
|
||||
/>
|
||||
<Font
|
||||
fontFamily="Rubik"
|
||||
fallbackFontFamily={["Helvetica", "Arial", "sans-serif"]}
|
||||
|
|
|
|||
|
|
@ -1,110 +1,91 @@
|
|||
export const unit = 16;
|
||||
|
||||
export const GREY_COLOR = [
|
||||
"#1A1A2E", //0
|
||||
"#2F2F41", //1
|
||||
"#444454", //2
|
||||
"#585867", //3
|
||||
"#6D6D7A", //4
|
||||
"#82828D", //5
|
||||
"#9797A0", //6
|
||||
"#ACACB3", //7
|
||||
"#C1C1C6", //8
|
||||
"#D5D5D9", //9
|
||||
"#EAEAEC", //10
|
||||
"#FFFFFF", //11
|
||||
];
|
||||
|
||||
export const BLUE_COLOR = "#395C6B";
|
||||
export const DANGER_COLOR = "#ED322C";
|
||||
export const TEXT_COLOR = GREY_COLOR[0];
|
||||
export const SECONDARY_COLOR = GREY_COLOR[5];
|
||||
export const DIMMED_COLOR = GREY_COLOR[7];
|
||||
export const DIVIDER_COLOR = GREY_COLOR[10];
|
||||
export const BACKGROUND_COLOR = "#F0F0F1";
|
||||
export const SURFACE_COLOR = DIVIDER_COLOR;
|
||||
export const SURFACE_DIVIDER_COLOR = GREY_COLOR[9];
|
||||
// @ts-nocheck
|
||||
export const unit = 12
|
||||
export const PRIMARY_COLOR = "#211E1E"
|
||||
export const TEXT_COLOR = "#656363"
|
||||
export const LINK_COLOR = "#007AFF"
|
||||
export const LINK_BACKGROUND_COLOR = "#F9F8F8"
|
||||
export const BACKGROUND_COLOR = "#F0F0F1"
|
||||
export const SURFACE_DIVIDER_COLOR = "#D5D5D9"
|
||||
|
||||
export const body = {
|
||||
background: BACKGROUND_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
export const container = {
|
||||
minWidth: "600px",
|
||||
};
|
||||
|
||||
export const medium = {
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
export const danger = {
|
||||
color: DANGER_COLOR,
|
||||
};
|
||||
padding: "64px 0px",
|
||||
}
|
||||
|
||||
export const frame = {
|
||||
padding: `${unit * 1.5}px`,
|
||||
padding: `${unit * 2}px`,
|
||||
border: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||
background: "#FFF",
|
||||
borderRadius: "6px",
|
||||
boxShadow: `0 1px 2px rgba(0,0,0,0.03),
|
||||
0 2px 4px rgba(0,0,0,0.03),
|
||||
0 2px 6px rgba(0,0,0,0.03)`,
|
||||
};
|
||||
}
|
||||
|
||||
export const textColor = {
|
||||
export const baseText = {
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
}
|
||||
|
||||
export const headingText = {
|
||||
color: PRIMARY_COLOR,
|
||||
fontSize: "16px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 500,
|
||||
lineHeight: "normal",
|
||||
}
|
||||
|
||||
export const contentText = {
|
||||
color: TEXT_COLOR,
|
||||
};
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 400,
|
||||
lineHeight: "180%",
|
||||
}
|
||||
|
||||
export const code = {
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
};
|
||||
export const buttonText = {
|
||||
color: "#FDFCFC",
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
}
|
||||
|
||||
export const headingHr = {
|
||||
margin: `${unit}px 0`,
|
||||
};
|
||||
|
||||
export const buttonPrimary = {
|
||||
...code,
|
||||
padding: "12px 18px",
|
||||
color: "#FFF",
|
||||
export const linkText = {
|
||||
color: LINK_COLOR,
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 400,
|
||||
lineHeight: "150%",
|
||||
textDecorationLine: "underline",
|
||||
textDecorationStyle: "solid" as const,
|
||||
textDecorationSkipInk: "auto" as const,
|
||||
textDecorationThickness: "auto",
|
||||
textUnderlineOffset: "auto",
|
||||
textUnderlinePosition: "from-font",
|
||||
borderRadius: "4px",
|
||||
background: BLUE_COLOR,
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
};
|
||||
background: LINK_BACKGROUND_COLOR,
|
||||
padding: "8px 12px",
|
||||
textAlign: "center" as const,
|
||||
}
|
||||
|
||||
export const compactText = {
|
||||
margin: "0 0 2px",
|
||||
};
|
||||
export const contentHighlightText = {
|
||||
color: PRIMARY_COLOR,
|
||||
}
|
||||
|
||||
export const breadcrumb = {
|
||||
fontSize: "14px",
|
||||
color: SECONDARY_COLOR,
|
||||
};
|
||||
|
||||
export const breadcrumbColonSeparator = {
|
||||
padding: " 0 4px",
|
||||
color: DIMMED_COLOR,
|
||||
};
|
||||
|
||||
export const breadcrumbSeparator = {
|
||||
color: DIVIDER_COLOR,
|
||||
};
|
||||
|
||||
export const heading = {
|
||||
fontSize: "22px",
|
||||
fontWeight: 500,
|
||||
};
|
||||
|
||||
export const sectionLabel = {
|
||||
...code,
|
||||
...compactText,
|
||||
letterSpacing: "0.5px",
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: DIMMED_COLOR,
|
||||
};
|
||||
|
||||
export const footerLink = {
|
||||
fontSize: "14px",
|
||||
};
|
||||
export const button = {
|
||||
display: "inline-grid",
|
||||
padding: "8px 12px 8px 20px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
flexShrink: "0",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: PRIMARY_COLOR,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
// @ts-nocheck
|
||||
import React from "react"
|
||||
import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
|
||||
import { Hr, Text, Fonts, SplitString, Title, A, Span, B } from "../components"
|
||||
import { Text, Fonts, Title, A, Span } from "../components"
|
||||
import {
|
||||
unit,
|
||||
body,
|
||||
code,
|
||||
frame,
|
||||
medium,
|
||||
heading,
|
||||
headingText,
|
||||
container,
|
||||
headingHr,
|
||||
footerLink,
|
||||
breadcrumb,
|
||||
compactText,
|
||||
buttonPrimary,
|
||||
breadcrumbColonSeparator,
|
||||
contentText,
|
||||
button,
|
||||
contentHighlightText,
|
||||
linkText,
|
||||
buttonText,
|
||||
} from "../styles"
|
||||
|
||||
const LOCAL_ASSETS_URL = "/static"
|
||||
const CONSOLE_URL = "https://opencode.ai/"
|
||||
const DOC_URL = "https://opencode.ai/docs/zen"
|
||||
|
||||
interface InviteEmailProps {
|
||||
inviter: string
|
||||
|
|
@ -32,9 +27,8 @@ export const InviteEmail = ({
|
|||
inviter = "test@anoma.ly",
|
||||
workspaceID = "wrk_01K6XFY7V53T8XN0A7X8G9BTN3",
|
||||
workspaceName = "anomaly",
|
||||
assetsUrl = LOCAL_ASSETS_URL,
|
||||
assetsUrl = `${CONSOLE_URL}email`,
|
||||
}: InviteEmailProps) => {
|
||||
const subject = `You've been invited to join the ${workspaceName} workspace on OpenCode Console`
|
||||
const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace.`
|
||||
const url = `${CONSOLE_URL}workspace/${workspaceID}`
|
||||
return (
|
||||
|
|
@ -55,50 +49,29 @@ export const InviteEmail = ({
|
|||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row style={headingHr}>
|
||||
<Column>
|
||||
<Hr />
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||
<Text style={{ ...compactText }}>
|
||||
<B>{inviter}</B> invited you to join the{" "}
|
||||
<Link style={medium} href={url}>
|
||||
<B>{workspaceName}</B>
|
||||
</Link>{" "}
|
||||
workspace in the{" "}
|
||||
<Link style={medium} href={`${CONSOLE_URL}zen`}>
|
||||
OpenCode Console
|
||||
</Link>
|
||||
.
|
||||
<Section style={{ padding: `${unit * 2}px 0 0 0` }}>
|
||||
<Text style={headingText}>Join your team's OpenCode workspace</Text>
|
||||
<Text style={contentText}>
|
||||
You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join the{" "}
|
||||
<Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||
<Button style={buttonPrimary} href={url}>
|
||||
<Span style={code}>Join Workspace</Span>
|
||||
<Button style={button} href={url}>
|
||||
<Text style={buttonText}>
|
||||
Join workspace
|
||||
<Img width="24" height="24" src={`${assetsUrl}/right-arrow.png`} alt="Arrow right" />
|
||||
</Text>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Row style={headingHr}>
|
||||
<Column>
|
||||
<Hr />
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Column>
|
||||
<Link href={`${CONSOLE_URL}zen`} style={footerLink}>
|
||||
Console
|
||||
</Link>
|
||||
</Column>
|
||||
<Column align="right">
|
||||
<Link style={footerLink} href={DOC_URL}>
|
||||
About
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||
<Text style={contentText}>Button not working? Copy the following link...</Text>
|
||||
<Link href={url}>
|
||||
<Text style={linkText}>{url}</Text>
|
||||
</Link>
|
||||
</Section>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
BIN
packages/console/mail/emails/templates/static/right-arrow.png
Normal file
BIN
packages/console/mail/emails/templates/static/right-arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 308 B |
|
|
@ -8,7 +8,7 @@
|
|||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||
"vite-plugin-solid": "catalog:"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"test": "bun test",
|
||||
"build": "./script/build.ts",
|
||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
"zod-to-json-schema": "3.24.5",
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opentui/core": "0.0.0-20251010-2eed09fd",
|
||||
"@opentui/solid": "0.0.0-20251010-2eed09fd",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Global } from "../global"
|
|||
import { ProjectRoute } from "./project"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { SessionLock } from "../session/lock"
|
||||
import { SessionPrompt } from "../session/prompt"
|
||||
import { SessionCompaction } from "../session/compaction"
|
||||
import { SessionRevert } from "../session/revert"
|
||||
|
|
@ -549,7 +550,7 @@ export namespace Server {
|
|||
}),
|
||||
),
|
||||
async (c) => {
|
||||
return c.json(SessionPrompt.abort(c.req.valid("param").id))
|
||||
return c.json(SessionLock.abort(c.req.valid("param").id))
|
||||
},
|
||||
)
|
||||
.post(
|
||||
|
|
|
|||
94
packages/opencode/src/session/lock.ts
Normal file
94
packages/opencode/src/session/lock.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import z from "zod/v4"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Log } from "../util/log"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace SessionLock {
|
||||
const log = Log.create({ service: "session.lock" })
|
||||
|
||||
export const LockedError = NamedError.create(
|
||||
"SessionLockedError",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
type LockState = {
|
||||
controller: AbortController
|
||||
created: number
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const locks = new Map<string, LockState>()
|
||||
return {
|
||||
locks,
|
||||
}
|
||||
},
|
||||
async (current) => {
|
||||
for (const [sessionID, lock] of current.locks) {
|
||||
log.info("force abort", { sessionID })
|
||||
lock.controller.abort()
|
||||
}
|
||||
current.locks.clear()
|
||||
},
|
||||
)
|
||||
|
||||
function get(sessionID: string) {
|
||||
return state().locks.get(sessionID)
|
||||
}
|
||||
|
||||
function unset(input: { sessionID: string; controller: AbortController }) {
|
||||
const lock = get(input.sessionID)
|
||||
if (!lock) return false
|
||||
if (lock.controller !== input.controller) return false
|
||||
state().locks.delete(input.sessionID)
|
||||
return true
|
||||
}
|
||||
|
||||
export function acquire(input: { sessionID: string }) {
|
||||
const lock = get(input.sessionID)
|
||||
if (lock) {
|
||||
throw new LockedError({ sessionID: input.sessionID, message: `Session ${input.sessionID} is locked` })
|
||||
}
|
||||
const controller = new AbortController()
|
||||
state().locks.set(input.sessionID, {
|
||||
controller,
|
||||
created: Date.now(),
|
||||
})
|
||||
log.info("locked", { sessionID: input.sessionID })
|
||||
return {
|
||||
signal: controller.signal,
|
||||
abort() {
|
||||
controller.abort()
|
||||
unset({ sessionID: input.sessionID, controller })
|
||||
},
|
||||
async [Symbol.dispose]() {
|
||||
const removed = unset({ sessionID: input.sessionID, controller })
|
||||
if (removed) {
|
||||
log.info("unlocked", { sessionID: input.sessionID })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function abort(sessionID: string) {
|
||||
const lock = get(sessionID)
|
||||
if (!lock) return false
|
||||
log.info("abort", { sessionID })
|
||||
lock.controller.abort()
|
||||
state().locks.delete(sessionID)
|
||||
return true
|
||||
}
|
||||
|
||||
export function isLocked(sessionID: string) {
|
||||
return get(sessionID) !== undefined
|
||||
}
|
||||
|
||||
export function assertUnlocked(sessionID: string) {
|
||||
const lock = get(sessionID)
|
||||
if (!lock) return
|
||||
throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
jsonSchema,
|
||||
} from "ai"
|
||||
import { SessionCompaction } from "./compaction"
|
||||
import { SessionLock } from "./lock"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Bus } from "../bus"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
|
|
@ -65,7 +66,6 @@ export namespace SessionPrompt {
|
|||
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const pending = new Map<string, AbortController>()
|
||||
const queued = new Map<
|
||||
string,
|
||||
{
|
||||
|
|
@ -75,14 +75,11 @@ export namespace SessionPrompt {
|
|||
>()
|
||||
|
||||
return {
|
||||
pending,
|
||||
queued,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
for (const [_, controller] of state.pending) {
|
||||
controller.abort()
|
||||
}
|
||||
async (current) => {
|
||||
current.queued.clear()
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1170,30 +1167,20 @@ export namespace SessionPrompt {
|
|||
}
|
||||
|
||||
function isBusy(sessionID: string) {
|
||||
return state().pending.has(sessionID)
|
||||
}
|
||||
|
||||
export function abort(sessionID: string) {
|
||||
const controller = state().pending.get(sessionID)
|
||||
if (!controller) return false
|
||||
log.info("aborting", {
|
||||
sessionID,
|
||||
})
|
||||
controller.abort()
|
||||
state().pending.delete(sessionID)
|
||||
return true
|
||||
return SessionLock.isLocked(sessionID)
|
||||
}
|
||||
|
||||
function lock(sessionID: string) {
|
||||
const handle = SessionLock.acquire({
|
||||
sessionID,
|
||||
})
|
||||
log.info("locking", { sessionID })
|
||||
if (state().pending.has(sessionID)) throw new Error("TODO")
|
||||
const controller = new AbortController()
|
||||
state().pending.set(sessionID, controller)
|
||||
return {
|
||||
signal: controller.signal,
|
||||
signal: handle.signal,
|
||||
abort: handle.abort,
|
||||
async [Symbol.dispose]() {
|
||||
handle[Symbol.dispose]()
|
||||
log.info("unlocking", { sessionID })
|
||||
state().pending.delete(sessionID)
|
||||
|
||||
const session = await Session.get(sessionID)
|
||||
if (session.parentID) return
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Log } from "../util/log"
|
|||
import { splitWhen } from "remeda"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Bus } from "../bus"
|
||||
import { SessionLock } from "./lock"
|
||||
|
||||
export namespace SessionRevert {
|
||||
const log = Log.create({ service: "session.revert" })
|
||||
|
|
@ -19,6 +20,11 @@ export namespace SessionRevert {
|
|||
export type RevertInput = z.infer<typeof RevertInput>
|
||||
|
||||
export async function revert(input: RevertInput) {
|
||||
SessionLock.assertUnlocked(input.sessionID)
|
||||
using _ = SessionLock.acquire({
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
|
||||
const all = await Session.messages(input.sessionID)
|
||||
let lastUser: MessageV2.User | undefined
|
||||
const session = await Session.get(input.sessionID)
|
||||
|
|
@ -64,6 +70,10 @@ export namespace SessionRevert {
|
|||
|
||||
export async function unrevert(input: { sessionID: string }) {
|
||||
log.info("unreverting", input)
|
||||
SessionLock.assertUnlocked(input.sessionID)
|
||||
using _ = SessionLock.acquire({
|
||||
sessionID: input.sessionID,
|
||||
})
|
||||
const session = await Session.get(input.sessionID)
|
||||
if (!session.revert) return session
|
||||
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Bus } from "../bus"
|
|||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { SessionLock } from "../session/lock"
|
||||
import { SessionPrompt } from "../session/prompt"
|
||||
|
||||
export const TaskTool = Tool.define("task", async () => {
|
||||
|
|
@ -53,7 +54,7 @@ export const TaskTool = Tool.define("task", async () => {
|
|||
}
|
||||
|
||||
ctx.abort.addEventListener("abort", () => {
|
||||
SessionPrompt.abort(session.id)
|
||||
SessionLock.abort(session.id)
|
||||
})
|
||||
const result = await SessionPrompt.prompt({
|
||||
messageID,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import z from "zod/v4"
|
||||
// import { Log } from "./log"
|
||||
|
||||
// const log = Log.create()
|
||||
|
||||
export abstract class NamedError extends Error {
|
||||
abstract schema(): z.core.$ZodType
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
"types": [],
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"customConditions": ["browser"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@tui/*": ["./src/cli/cmd/tui/*"]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.15.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"build": "tsc"
|
||||
},
|
||||
"exports": {
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.15.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"build": "./script/build.ts"
|
||||
},
|
||||
"exports": {
|
||||
|
|
@ -19,10 +19,11 @@
|
|||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {},
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -145,4 +146,9 @@
|
|||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.root {
|
||||
position: relative;
|
||||
color: var(--sl-color-text);
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
|
|
@ -54,4 +55,9 @@
|
|||
&[data-theme="blue"] {
|
||||
background-color: var(--sl-color-blue-low);
|
||||
}
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,11 +127,6 @@
|
|||
flex-grow: 1;
|
||||
max-width: var(--md-tool-width);
|
||||
position: relative;
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="assistant-reasoning"] {
|
||||
|
|
@ -149,11 +144,6 @@
|
|||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,11 +162,6 @@
|
|||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,11 +285,6 @@
|
|||
padding: 0.5rem calc(0.5rem + 3px);
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
|
||||
[data-component="copy-button"] {
|
||||
top: 0.5rem;
|
||||
right: calc(0.5rem - 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,48 +3,52 @@ title: MCP servers
|
|||
description: Add local and remote MCP tools.
|
||||
---
|
||||
|
||||
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both:
|
||||
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP.
|
||||
|
||||
OpenCode supports both:
|
||||
|
||||
- Local servers
|
||||
- Remote servers
|
||||
|
||||
Once added, MCP tools are automatically available to the LLM alongside built-in tools.
|
||||
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
You can define MCP servers in your OpenCode config under `mcp`.
|
||||
:::note
|
||||
OAuth support for MCP servers is coming soon.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Local
|
||||
## Caveats
|
||||
|
||||
Add local MCP servers using `"type": "local"` within the MCP object. Multiple MCP servers can be added.
|
||||
When you use an MCP server, it adds to the context. This can quickly add up if
|
||||
you have a lot of tools. So we recommend being careful with which MCP servers
|
||||
you use.
|
||||
|
||||
:::tip
|
||||
MCP servers add to your context, so you want to be careful with which
|
||||
ones you enable.
|
||||
:::
|
||||
|
||||
The key string for each server can be any arbitrary name.
|
||||
Certain MCP servers, like the GitHub MCP server tend to add a lot of tokens and
|
||||
can easily exceed the context limit.
|
||||
|
||||
```json title="opencode.json" {15}
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
You can define MCP servers in your OpenCode config under `mcp`. Add each MCP
|
||||
with a unique name. You can refer to that MCP by name when prompting the LLM.
|
||||
|
||||
```jsonc title="opencode.jsonc" {6}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-local-mcp-server": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
},
|
||||
"my-different-local-mcp-server": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-other-mcp-command"],
|
||||
"name-of-mcp-server": {
|
||||
// ...
|
||||
"enabled": true
|
||||
},
|
||||
"name-of-other-mcp-server": {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,40 +58,71 @@ You can also disable a server by setting `enabled` to `false`. This is useful if
|
|||
|
||||
---
|
||||
|
||||
### Remote
|
||||
### Local
|
||||
|
||||
Add remote MCP servers under `mcp` with `"type": "remote"`.
|
||||
Add local MCP servers using `type` to `"local"` within the MCP object.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-remote-mcp": {
|
||||
"type": "remote",
|
||||
"url": "https://my-mcp-server.com",
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer MY_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Local and remote servers can be used together within the same `mcp` config object.
|
||||
|
||||
```json title="opencode.json"
|
||||
```jsonc title="opencode.jsonc" {15}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-local-mcp-server": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
// Or ["bun", "x", "my-mcp-command"]
|
||||
"command": ["npx", "-y", "my-mcp-command"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The command is how the local MCP server is started. You can also pass in a list of environment variables as well.
|
||||
|
||||
For example, here's how I can add the test
|
||||
[`@modelcontextprotocol/server-everything`](https://www.npmjs.com/package/@modelcontextprotocol/server-everything) MCP server.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"mcp_everything": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And to use it I can add `use the mcp_everything tool` to my prompts.
|
||||
|
||||
```txt "mcp_everything"
|
||||
use the mcp_everything tool to add the number 3 and 4
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
Here are all the options for configuring a local MCP server.
|
||||
|
||||
| Option | Type | Required | Description |
|
||||
| ------------- | ------- | -------- | ----------------------------------------------------- |
|
||||
| `type` | String | Y | Type of MCP server connection, must be `"local"`. |
|
||||
| `command` | Array | Y | Command and arguments to run the MCP server. |
|
||||
| `environment` | Object | | Environment variables to set when running the server. |
|
||||
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
|
||||
|
||||
---
|
||||
|
||||
### Remote
|
||||
|
||||
Add remote MCP servers under by setting `type` to `"remote"`.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"my-remote-mcp": {
|
||||
"type": "remote",
|
||||
"url": "https://my-mcp-server.com",
|
||||
|
|
@ -100,6 +135,17 @@ Local and remote servers can be used together within the same `mcp` config objec
|
|||
}
|
||||
```
|
||||
|
||||
Here the `url` is the URL of the remote MCP server and with the `headers` option you can pass in a list of headers.
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Type | Required | Description |
|
||||
| --------- | ------- | -------- | -------------------------------------------------- |
|
||||
| `type` | String | Y | Type of MCP server connection, must be `"remote"`. |
|
||||
| `url` | String | Y | URL of the remote MCP server. |
|
||||
| `enabled` | Boolean | | Enable or disable the MCP server on startup. |
|
||||
| `headers` | Object | | Headers to send with the request. |
|
||||
|
||||
---
|
||||
|
||||
## Manage
|
||||
|
|
@ -197,3 +243,90 @@ The glob pattern uses simple regex globbing patterns.
|
|||
- `*` matches zero or more of any character
|
||||
- `?` matches exactly one character
|
||||
- All other characters match literally
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
Below are examples of some common MCP servers. You can submit a PR if you want to document other servers.
|
||||
|
||||
---
|
||||
|
||||
### Context7
|
||||
|
||||
Add the [Context7 MCP server](https://github.com/context-labs/mcp-server-context7) to search through docs.
|
||||
|
||||
```json title="opencode.json" {4-7}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you have signed up for a free account, you can use your API key and get higher rate-limits.
|
||||
|
||||
```json title="opencode.json" {7-9}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/mcp",
|
||||
"headers": {
|
||||
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we are assuming that you have the `CONTEXT7_API_KEY` environment variable set.
|
||||
|
||||
Add `use context7` to your prompts to use Context7 MCP server.
|
||||
|
||||
```txt "use context7"
|
||||
Configure a Cloudflare Worker script to cache JSON API responses for five minutes. use context7
|
||||
```
|
||||
|
||||
Alternatively, you can add something like this to your
|
||||
[AGENTS.md](/docs/rules/).
|
||||
|
||||
```md title="AGENTS.md"
|
||||
When you need to search docs, use `context7` tools.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Grep by Vercel
|
||||
|
||||
Add the [Grep by Vercel](https://github.com/vercel/grep-by-vercel) MCP server to search through code snippets on GitHub.
|
||||
|
||||
```json title="opencode.json" {4-7}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gh_grep": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.grep.app"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since we named our MCP server `gh_grep`, you can add `use the gh_grep tool` to your prompts to get the agent to use it.
|
||||
|
||||
```txt "use the gh_grep tool"
|
||||
What's the right way to set a custom domain in an SST Astro component? use the gh_grep tool
|
||||
```
|
||||
|
||||
Alternatively, you can add something like this to your
|
||||
[AGENTS.md](/docs/rules/).
|
||||
|
||||
```md title="AGENTS.md"
|
||||
If you are unsure how to do something, use `gh_grep` to search code examples from github.
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue