diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2df2814c..c8d867284 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 05a47895c..337379178 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -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. diff --git a/bun.lock b/bun.lock index 7ee247a34..6c2d60fd0 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index c12f61804..c1deb7420 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 5c4a63f14..c4019388e 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -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" diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 29b35bfa4..d6b3e2a43 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -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 }) {
  • - Login + {isLoggedIn() ? "Workspace" : "Login"} Zen @@ -110,7 +112,7 @@ export function Header(props: { zen?: boolean }) {
  • - Login + {isLoggedIn() ? "Workspace" : "Login"} Zen diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index ccf2dccfc..fc27ef3b4 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -118,3 +118,86 @@ export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes) { ) } + +export function IconOpenAI(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconAnthropic(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconXai(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} + +export function IconAlibaba(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconMoonshotAI(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconZai(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconStealth(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index d51e9ec18..287c25735 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -43,7 +43,7 @@ export default function Home() {
    OpenCode | The AI coding agent built for the terminal - +
    diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index ef79ec4fe..c979336e2 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -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 (
    +
    diff --git a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx index c830cee8a..0fb2a0df6 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx @@ -96,7 +96,7 @@ export function PaymentSection() { }} data-slot="receipt-button" > - view + View diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index 6d8bfb0ec..8f7678f21 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -52,7 +52,7 @@ export default function () { } > - Current balance: ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} + Current balance ${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()} diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.module.css b/packages/console/app/src/routes/workspace/[id]/model-section.module.css index 420545670..fa7684888 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/model-section.module.css @@ -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; } } } -} \ No newline at end of file +} diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index 1740c9d3c..be3cffc47 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -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 (
    @@ -58,16 +78,40 @@ export function ModelSection() { Model + Enabled - - {({ id, name }) => { + + {({ id, name, lab }) => { const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id)) return ( - {name} + +
    + {(() => { + switch (lab) { + case "OpenAI": + return + case "Anthropic": + return + case "Moonshot AI": + return + case "Z.ai": + return + case "Alibaba": + return + case "xAI": + return + default: + return + } + })()} + {name} +
    + + {lab}
    diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index fef1b3cd9..625693223 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -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(() => { diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 05c523235..080070d4f 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -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 (
    @@ -102,7 +105,7 @@ export default function Home() {
    - Get started with Zen + {isLoggedIn() ? "Go to workspace " : "Get started with Zen "} x?.id) if (!accountID) { console.log("creating account for", email) diff --git a/packages/console/mail/emails/components.tsx b/packages/console/mail/emails/components.tsx index ff845c8f4..a28e29f78 100644 --- a/packages/console/mail/emails/components.tsx +++ b/packages/console/mail/emails/components.tsx @@ -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 -} - -export function Hr(props: HrProps) { - return -} - -export function SurfaceHr(props: HrProps) { - return ( - - ) + return } 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 ( <> - - { - 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 = ({ - - -
    - - - -
    - - {inviter} invited you to join the{" "} - - {workspaceName} - {" "} - workspace in the{" "} - - OpenCode Console - - . +
    + Join your team's OpenCode workspace + + You have been invited by {inviter} to join the{" "} + {workspaceName} workspace on OpenCode.
    -
    - - -
    -
    -
    - - - - - Console - - - - - About - - - +
    + Button not working? Copy the following link... + + {url} + +
    diff --git a/packages/console/mail/emails/templates/static/JetBrainsMono-Medium.woff2 b/packages/console/mail/emails/templates/static/JetBrainsMono-Medium.woff2 new file mode 100644 index 000000000..669d04cdf Binary files /dev/null and b/packages/console/mail/emails/templates/static/JetBrainsMono-Medium.woff2 differ diff --git a/packages/console/mail/emails/templates/static/JetBrainsMono-Regular.woff2 b/packages/console/mail/emails/templates/static/JetBrainsMono-Regular.woff2 new file mode 100644 index 000000000..40da42765 Binary files /dev/null and b/packages/console/mail/emails/templates/static/JetBrainsMono-Regular.woff2 differ diff --git a/packages/console/mail/emails/templates/static/right-arrow.png b/packages/console/mail/emails/templates/static/right-arrow.png new file mode 100644 index 000000000..9f90bcb5c Binary files /dev/null and b/packages/console/mail/emails/templates/static/right-arrow.png differ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ba5aabf08..92f656a4d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -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:" diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7b1350bca..86585201a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 26f72bf74..c39176a02 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -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( diff --git a/packages/opencode/src/session/lock.ts b/packages/opencode/src/session/lock.ts new file mode 100644 index 000000000..4b510dc97 --- /dev/null +++ b/packages/opencode/src/session/lock.ts @@ -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() + 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` }) + } +} diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d4666c3c3..a6dabcad2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -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() 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 diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 052e582f1..0b0f4294f 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -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 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) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 302e0cce3..95f650e01 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -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, diff --git a/packages/opencode/src/util/error.ts b/packages/opencode/src/util/error.ts index f93c4d714..6e5414f46 100644 --- a/packages/opencode/src/util/error.ts +++ b/packages/opencode/src/util/error.ts @@ -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 diff --git a/packages/opencode/tsconfig.json b/packages/opencode/tsconfig.json index 63709af04..9067d84fd 100644 --- a/packages/opencode/tsconfig.json +++ b/packages/opencode/tsconfig.json @@ -8,7 +8,6 @@ "types": [], "noUncheckedIndexedAccess": false, "customConditions": ["browser"], - "baseUrl": ".", "paths": { "@/*": ["./src/*"], "@tui/*": ["./src/cli/cmd/tui/*"] diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 48332ca59..3473940a0 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -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:" } -} \ No newline at end of file +} diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index c74d60409..3fd345f9e 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/slack/package.json b/packages/slack/package.json index 9da2f2284..cc060a269 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -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:" } } diff --git a/packages/web/src/components/share/content-markdown.module.css b/packages/web/src/components/share/content-markdown.module.css index 765c25930..858a8adea 100644 --- a/packages/web/src/components/share/content-markdown.module.css +++ b/packages/web/src/components/share/content-markdown.module.css @@ -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; + } } diff --git a/packages/web/src/components/share/content-text.module.css b/packages/web/src/components/share/content-text.module.css index a3842275c..139aa3faa 100644 --- a/packages/web/src/components/share/content-text.module.css +++ b/packages/web/src/components/share/content-text.module.css @@ -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); + } } diff --git a/packages/web/src/components/share/part.module.css b/packages/web/src/components/share/part.module.css index 45310a0b2..b1269445f 100644 --- a/packages/web/src/components/share/part.module.css +++ b/packages/web/src/components/share/part.module.css @@ -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); - } } } } diff --git a/packages/web/src/content/docs/mcp-servers.mdx b/packages/web/src/content/docs/mcp-servers.mdx index 535e22839..4dc77b954 100644 --- a/packages/web/src/content/docs/mcp-servers.mdx +++ b/packages/web/src/content/docs/mcp-servers.mdx @@ -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. +```