diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8be1b4dba..41ca2f1cf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,6 +17,7 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
+- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.
diff --git a/bun.lock b/bun.lock
index ea2a44d73..07a28babc 100644
--- a/bun.lock
+++ b/bun.lock
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -111,7 +111,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -152,7 +152,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -168,7 +168,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "0.15.24",
+ "version": "0.15.28",
"bin": {
"opencode": "./bin/opencode",
},
@@ -228,6 +228,10 @@
"@babel/core": "7.28.4",
"@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",
@@ -244,7 +248,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -264,7 +268,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "0.15.24",
+ "version": "0.15.28",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -275,7 +279,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -288,7 +292,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "catalog:",
@@ -311,7 +315,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 906161219..9bd3ae9bf 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -7,7 +7,7 @@
"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",
"start": "vinxi start",
- "version": "0.15.24"
+ "version": "0.15.28"
},
"dependencies": {
"@ibm/plex": "6.4.1",
diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx
index 29b35bfa4..ea8921f30 100644
--- a/packages/console/app/src/component/header.tsx
+++ b/packages/console/app/src/component/header.tsx
@@ -36,6 +36,9 @@ export function Header(props: { zen?: boolean }) {
Docs
+
+ Enterprise
+
@@ -107,6 +110,9 @@ export function Header(props: { zen?: boolean }) {
Docs
+
+ Enterprise
+
diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts
new file mode 100644
index 000000000..d937be543
--- /dev/null
+++ b/packages/console/app/src/routes/api/enterprise.ts
@@ -0,0 +1,50 @@
+import type { APIEvent } from "@solidjs/start/server"
+import { AWS } from "@opencode-ai/console-core/aws.js"
+
+interface EnterpriseFormData {
+ name: string
+ role: string
+ email: string
+ message: string
+}
+
+export async function POST(event: APIEvent) {
+ try {
+ const body = (await event.request.json()) as EnterpriseFormData
+
+ // Validate required fields
+ if (!body.name || !body.role || !body.email || !body.message) {
+ return Response.json({ error: "All fields are required" }, { status: 400 })
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ if (!emailRegex.test(body.email)) {
+ return Response.json({ error: "Invalid email format" }, { status: 400 })
+ }
+
+ // Create email content
+ const emailContent = `
+New Enterprise Inquiry
+
+Name: ${body.name}
+Role: ${body.role}
+Email: ${body.email}
+
+Message:
+${body.message}
+ `.trim()
+
+ // Send email using AWS SES
+ await AWS.sendEmail({
+ to: "enterprise@opencode.ai",
+ subject: `Enterprise Inquiry from ${body.name}`,
+ body: emailContent,
+ })
+
+ return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
+ } catch (error) {
+ console.error("Error processing enterprise form:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/packages/console/app/src/routes/enterprise/index.css b/packages/console/app/src/routes/enterprise/index.css
new file mode 100644
index 000000000..44c6f8611
--- /dev/null
+++ b/packages/console/app/src/routes/enterprise/index.css
@@ -0,0 +1,544 @@
+::selection {
+ background: var(--color-background-interactive);
+ color: var(--color-text-strong);
+
+ @media (prefers-color-scheme: dark) {
+ background: var(--color-background-interactive);
+ color: var(--color-text-inverted);
+ }
+}
+
+
+[data-page="enterprise"] {
+ --color-background: hsl(0, 20%, 99%);
+ --color-background-weak: hsl(0, 8%, 97%);
+ --color-background-weak-hover: hsl(0, 8%, 94%);
+ --color-background-strong: hsl(0, 5%, 12%);
+ --color-background-strong-hover: hsl(0, 5%, 18%);
+ --color-background-interactive: hsl(62, 84%, 88%);
+ --color-background-interactive-weaker: hsl(64, 74%, 95%);
+
+ --color-text: hsl(0, 1%, 39%);
+ --color-text-weak: hsl(0, 1%, 60%);
+ --color-text-weaker: hsl(30, 2%, 81%);
+ --color-text-strong: hsl(0, 5%, 12%);
+ --color-text-inverted: hsl(0, 20%, 99%);
+ --color-text-success: hsl(119, 100%, 35%);
+
+ --color-border: hsl(30, 2%, 81%);
+ --color-border-weak: hsl(0, 1%, 85%);
+
+ --color-icon: hsl(0, 1%, 55%);
+ --color-success: hsl(142, 76%, 36%);
+
+ background: var(--color-background);
+ font-family: var(--font-mono);
+ color: var(--color-text);
+ padding-bottom: 5rem;
+
+ @media (prefers-color-scheme: dark) {
+ --color-background: hsl(0, 9%, 7%);
+ --color-background-weak: hsl(0, 6%, 10%);
+ --color-background-weak-hover: hsl(0, 6%, 15%);
+ --color-background-strong: hsl(0, 15%, 94%);
+ --color-background-strong-hover: hsl(0, 15%, 97%);
+ --color-background-interactive: hsl(62, 100%, 90%);
+ --color-background-interactive-weaker: hsl(60, 20%, 8%);
+
+ --color-text: hsl(0, 4%, 71%);
+ --color-text-weak: hsl(0, 2%, 49%);
+ --color-text-weaker: hsl(0, 3%, 28%);
+ --color-text-strong: hsl(0, 15%, 94%);
+ --color-text-inverted: hsl(0, 9%, 7%);
+ --color-text-success: hsl(119, 60%, 72%);
+
+
+ --color-border: hsl(0, 3%, 28%);
+ --color-border-weak: hsl(0, 4%, 23%);
+
+ --color-icon: hsl(10, 3%, 43%);
+ --color-success: hsl(142, 76%, 46%);
+ }
+
+ /* Header and Footer styles - copied from index.css */
+ [data-component="top"] {
+ padding: 24px 5rem;
+ height: 80px;
+ position: sticky;
+ top: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--color-background);
+ border-bottom: 1px solid var(--color-border-weak);
+ z-index: 10;
+
+ @media (max-width: 60rem) {
+ padding: 24px 1.5rem;
+ }
+
+ img {
+ height: 34px;
+ width: auto;
+ }
+
+ [data-component="nav-desktop"] {
+ ul {
+ display: flex;
+ justify-content: space-between;
+ gap: 48px;
+ li {
+ display: inline-block;
+ a {
+ text-decoration: none;
+ span {
+ color: var(--color-text-weak);
+ }
+ }
+ a:hover {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+ }
+ }
+ }
+
+ @media (max-width: 40rem) {
+ display: none;
+ }
+ }
+
+ [data-component="nav-mobile"] {
+ button > svg {
+ color: var(--color-icon);
+ }
+ }
+
+ [data-component="nav-mobile-toggle"] {
+ border: none;
+ background: none;
+ outline: none;
+ height: 40px;
+ width: 40px;
+ cursor: pointer;
+ margin-right: -8px;
+ }
+
+ [data-component="nav-mobile-toggle"]:hover {
+ background: var(--color-background-weak);
+ }
+
+ [data-component="nav-mobile"] {
+ display: none;
+
+ @media (max-width: 40rem) {
+ display: block;
+
+ [data-component="nav-mobile-icon"] {
+ cursor: pointer;
+ height: 40px;
+ width: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ [data-component="nav-mobile-menu-list"] {
+ position: fixed;
+ background: var(--color-background);
+ top: 80px;
+ left: 0;
+ right: 0;
+ height: 100vh;
+
+ ul {
+ list-style: none;
+ padding: 20px 0;
+
+ li {
+ a {
+ text-decoration: none;
+ padding: 20px;
+ display: block;
+
+ span {
+ color: var(--color-text-weak);
+ }
+ }
+
+ a:hover {
+ background: var(--color-background-weak);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ [data-slot="logo dark"] {
+ display: none;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ [data-slot="logo light"] {
+ display: none;
+ }
+ [data-slot="logo dark"] {
+ display: block;
+ }
+ }
+ }
+
+ [data-component="footer"] {
+ border-top: 1px solid var(--color-border-weak);
+ display: flex;
+ flex-direction: row;
+
+ @media (max-width: 65rem) {
+ border-bottom: 1px solid var(--color-border-weak);
+ }
+
+ [data-slot="cell"] {
+ flex: 1;
+ text-align: center;
+
+ a {
+ text-decoration: none;
+ padding: 2rem 0;
+ width: 100%;
+ display: block;
+
+ span {
+ color: var(--color-text-weak);
+
+ @media (max-width: 40rem) {
+ display: none;
+ }
+ }
+ }
+
+ a:hover {
+ background: var(--color-background-weak);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+ }
+ }
+
+ [data-slot="cell"] + [data-slot="cell"] {
+ border-left: 1px solid var(--color-border-weak);
+
+ @media (max-width: 40rem) {
+ border-left: none;
+ }
+ }
+
+ /* Mobile: third column on its own row */
+ @media (max-width: 25rem) {
+ flex-wrap: wrap;
+
+ [data-slot="cell"] {
+ flex: 1 0 100%;
+ border-left: none;
+ border-top: 1px solid var(--color-border-weak);
+ }
+
+ [data-slot="cell"]:nth-child(1) {
+ border-top: none;
+ }
+ }
+ }
+
+ [data-component="container"] {
+ max-width: 67.5rem;
+ margin: 0 auto;
+ border: 1px solid var(--color-border-weak);
+ border-top: none;
+
+ @media (max-width: 65rem) {
+ border: none;
+ }
+ }
+
+ [data-component="content"] {
+ }
+
+ [data-component="enterprise-content"] {
+ padding: 4rem 0;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 0;
+ }
+ }
+
+ [data-component="enterprise-columns"] {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4rem;
+ padding: 4rem 5rem;
+
+ @media (max-width: 80rem) {
+ gap: 3rem;
+ }
+
+ @media (max-width: 60rem) {
+ grid-template-columns: 1fr;
+ gap: 3rem;
+ padding: 2rem 1.5rem;
+ }
+ }
+
+ [data-component="enterprise-column-1"] {
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 1rem;
+ }
+
+ h3 {
+ font-size: 1.25rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin: 2rem 0 1rem 0;
+ }
+
+ p {
+ line-height: 1.6;
+ margin-bottom: 1.5rem;
+ color: var(--color-text);
+ }
+
+ [data-component="testimonial"] {
+ margin-top: 4rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+
+ [data-component="quotation"] {
+ svg {
+ margin-bottom: 1rem;
+ opacity: 20%;
+ }
+ }
+
+ [data-component="testimonial-logo"] {
+ svg {
+ margin-top: 1.5rem;
+ }
+ }
+ }
+ }
+
+ [data-component="enterprise-column-2"] {
+ [data-component="enterprise-form"] {
+ padding: 0;
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 1.5rem;
+ }
+
+ [data-component="form-group"] {
+ margin-bottom: 1.5rem;
+
+ label {
+ display: block;
+ font-weight: 500;
+ color: var(--color-text-weak);
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+ }
+
+ input:-webkit-autofill,
+ input:-webkit-autofill:hover,
+ input:-webkit-autofill:focus,
+ input:-webkit-autofill:active {
+ transition: background-color 5000000s ease-in-out 0s;
+ }
+
+ input:-webkit-autofill {
+ -webkit-text-fill-color: var(--color-text-strong) !important;
+ }
+
+ input:-moz-autofill {
+ -moz-text-fill-color: var(--color-text-strong) !important;
+ }
+
+ input,
+ textarea {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid var(--color-border-weak);
+ border-radius: 4px;
+ background: var(--color-background-weak);
+ color: var(--color-text-strong);
+ font-family: inherit;
+
+ &::placeholder {
+ color: var(--color-text-weak);
+ }
+
+ &:focus {
+ background: var(--color-background-interactive-weaker);
+ outline: none;
+ border: none;
+ color: var(--color-text-strong);
+ border: 1px solid var(--color-background-strong);
+ box-shadow: 0 0 0 3px var(--color-background-interactive);
+
+ @media (prefers-color-scheme: dark) {
+ box-shadow: none;
+ border: 1px solid var(--color-background-interactive)
+ }
+ }
+ }
+
+ textarea {
+ resize: vertical;
+ min-height: 120px;
+ }
+ }
+
+ [data-component="submit-button"] {
+ padding: 0.5rem 1.5rem;
+ background: var(--color-background-strong);
+ color: var(--color-text-inverted);
+ border: none;
+ border-radius: 4px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover:not(:disabled) {
+ background: var(--color-background-strong-hover);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ [data-component="success-message"] {
+ margin-top: 1rem;
+ padding: 1rem 0;
+ color: var(--color-text-success);
+ text-align: left;
+ }
+ }
+ }
+
+ [data-component="faq"] {
+ border-top: 1px solid var(--color-border-weak);
+ padding: 4rem 5rem;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 1.5rem;
+ }
+
+ [data-slot="section-title"] {
+ margin-bottom: 24px;
+
+ h3 {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 12px;
+ }
+
+ p {
+ margin-bottom: 12px;
+ color: var(--color-text);
+ }
+ }
+
+ ul {
+ padding: 0;
+
+ li {
+ list-style: none;
+ margin-bottom: 24px;
+ line-height: 200%;
+
+ @media (max-width: 60rem) {
+ line-height: 180%;
+ }
+ }
+ }
+
+ [data-slot="faq-question"] {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 8px;
+ color: var(--color-text-strong);
+ font-weight: 500;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+
+ [data-slot="faq-icon-plus"] {
+ flex-shrink: 0;
+ color: var(--color-text-weak);
+ margin-top: 2px;
+
+ [data-closed] & {
+ display: block;
+ }
+ [data-expanded] & {
+ display: none;
+ }
+ }
+ [data-slot="faq-icon-minus"] {
+ flex-shrink: 0;
+ color: var(--color-text-weak);
+ margin-top: 2px;
+
+ [data-closed] & {
+ display: none;
+ }
+ [data-expanded] & {
+ display: block;
+ }
+ }
+ [data-slot="faq-question-text"] {
+ flex-grow: 1;
+ text-align: left;
+ }
+ }
+
+ [data-slot="faq-answer"] {
+ margin-left: 40px;
+ margin-bottom: 32px;
+ color: var(--color-text);
+ }
+ }
+
+ [data-component="legal"] {
+ color: var(--color-text-weak);
+ text-align: center;
+ padding: 2rem 5rem;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 1.5rem;
+ }
+
+ a {
+ color: var(--color-text-weak);
+ text-decoration: none;
+ }
+ }
+
+ a {
+ color: var(--color-text-strong);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+
+ &:hover {
+ text-decoration-thickness: 2px;
+ }
+ }
+}
diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx
new file mode 100644
index 000000000..9599ad38b
--- /dev/null
+++ b/packages/console/app/src/routes/enterprise/index.tsx
@@ -0,0 +1,254 @@
+import "./index.css"
+import { Title, Meta } from "@solidjs/meta"
+import { createSignal } from "solid-js"
+import { Header } from "~/component/header"
+import { Footer } from "~/component/footer"
+import { Legal } from "~/component/legal"
+import { Faq } from "~/component/faq"
+
+export default function Enterprise() {
+ const [formData, setFormData] = createSignal({
+ name: "",
+ role: "",
+ email: "",
+ message: "",
+ })
+ const [isSubmitting, setIsSubmitting] = createSignal(false)
+ const [showSuccess, setShowSuccess] = createSignal(false)
+
+ const handleInputChange = (field: string) => (e: Event) => {
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement
+ setFormData((prev) => ({ ...prev, [field]: target.value }))
+ }
+
+ const handleSubmit = async (e: Event) => {
+ e.preventDefault()
+ setIsSubmitting(true)
+
+ try {
+ const response = await fetch("/api/enterprise", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData()),
+ })
+
+ if (response.ok) {
+ setShowSuccess(true)
+ setFormData({
+ name: "",
+ role: "",
+ email: "",
+ message: "",
+ })
+ setTimeout(() => setShowSuccess(false), 5000)
+ }
+ } catch (error) {
+ console.error("Failed to submit form:", error)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+
+ OpenCode | Enterprise solutions for your organisation
+
+
+
+
+
+
+
+
+
Your code is yours
+
+ OpenCode operates securely inside your organization with no data or context stored and no licensing restrictions or ownership claims. Start a trial with your team today, then scale confidently with enterprise-grade features including SSO, private registries, and self-hosting.
+
+
+ Let us know and how we can help.
+
+
+
+
+
+ Thanks to OpenCode, we found a way to create software to track all our assets — even the imaginary ones.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSuccess() && (
+
+ Message sent, we'll be in touch soon.
+
+ )}
+
+
+
+
+
+
+
+
FAQ
+
+
+
+
+ No. OpenCode never stores your code or context data. All
+ processing happens locally or directly with your AI provider.
+
+
+
+
+ You do. All code produced is yours, with no licensing
+ restrictions or ownership claims.
+
+
+
+
+ Simply install and run an internal trial with your team. Since
+ OpenCode doesn’t store any data, your developers can get
+ started right away.
+
+
+
+
+ By default, sharing is disabled. If enabled, conversations are
+ sent to our share service and cached through our CDN. For
+ enterprise use, we recommend disabling or self-hosting this
+ feature.
+
+
+
+
+ Yes. Enterprise deployments can include SSO integration so all
+ sessions and shared conversations are protected by your
+ authentication system.
+
+
+
+
+ Absolutely. You can fully self-host OpenCode, including the
+ share feature, ensuring that data and pages are accessible
+ only after authentication.
+
+
+
+
+ Contact us to discuss pricing, implementation, and enterprise
+ options like SSO, private registries, and self-hosting.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css
index 7171bd39d..b46958b80 100644
--- a/packages/console/app/src/routes/index.css
+++ b/packages/console/app/src/routes/index.css
@@ -827,12 +827,8 @@ body {
outline: none;
border: none;
color: var(--color-text-strong);
-
- border: 1px solid var(--color-background-strong); /* Tailwind blue-600 as example */
-
- /* Tailwind-style ring */
+ border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
- /* mimics "ring-2 ring-blue-600/50" */
@media (prefers-color-scheme: dark) {
box-shadow: none;
diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
index 807f427af..603d8917b 100644
--- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts
+++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts
@@ -188,6 +188,7 @@ export function fromAnthropicRequest(body: any): CommonRequest {
})()
return {
+ model: body.model,
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
index cad6bd686..daf650275 100644
--- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
+++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
@@ -114,6 +114,7 @@ export function fromOaCompatibleRequest(body: any): CommonRequest {
}
return {
+ model: body.model,
max_tokens: body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts
index 21c15f355..d17300991 100644
--- a/packages/console/app/src/routes/zen/util/provider/openai.ts
+++ b/packages/console/app/src/routes/zen/util/provider/openai.ts
@@ -177,6 +177,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
})()
return {
+ model: body.model,
max_tokens: body.max_output_tokens ?? body.max_tokens,
temperature: body.temperature,
top_p: body.top_p,
@@ -310,7 +311,7 @@ export function toOpenaiRequest(body: CommonRequest) {
metadata: (body as any).metadata,
store: (body as any).store,
user: (body as any).user,
- text: { verbosity: "low" },
+ text: { verbosity: body.model === "gpt-5-codex" ? "medium" : "low" },
reasoning: { effort: "medium" },
}
}
diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts
index c8ba644ba..03dfbc3c6 100644
--- a/packages/console/app/src/routes/zen/util/provider/provider.ts
+++ b/packages/console/app/src/routes/zen/util/provider/provider.ts
@@ -95,7 +95,7 @@ export interface CommonUsage {
}
export interface CommonRequest {
- model?: string
+ model: string
max_tokens?: number
temperature?: number
top_p?: number
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 31740b902..b084bcd22 100644
--- a/packages/console/core/package.json
+++ b/packages/console/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
- "version": "0.15.24",
+ "version": "0.15.28",
"private": true,
"type": "module",
"dependencies": {
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index fec9b5d09..179ee007d 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "0.15.24",
+ "version": "0.15.28",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json
index 7598cb2b4..5aa23d2cc 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "0.15.24",
+ "version": "0.15.28",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 3a54ec26f..cf6057880 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
- "version": "0.15.24",
+ "version": "0.15.28",
"description": "",
"type": "module",
"scripts": {
diff --git a/packages/desktop/src/components/assistant-message.tsx b/packages/desktop/src/components/message.tsx
similarity index 88%
rename from packages/desktop/src/components/assistant-message.tsx
rename to packages/desktop/src/components/message.tsx
index 8c654660b..589ca3118 100644
--- a/packages/desktop/src/components/assistant-message.tsx
+++ b/packages/desktop/src/components/message.tsx
@@ -1,4 +1,4 @@
-import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart, Message } from "@opencode-ai/sdk"
+import type { Part, ReasoningPart, TextPart, ToolPart, Message, AssistantMessage, UserMessage } from "@opencode-ai/sdk"
import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Markdown } from "./markdown"
@@ -17,7 +17,20 @@ import type { WriteTool } from "opencode/tool/write"
import type { TodoWriteTool } from "opencode/tool/todo"
import { DiffChanges } from "./diff-changes"
-export function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
+export function Message(props: { message: Message; parts: Part[] }) {
+ return (
+
+
+ {(userMessage) => }
+
+
+ {(assistantMessage) => }
+
+
+ )
+}
+
+function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
const filteredParts = createMemo(() => {
return props.parts?.filter((x) => {
if (x.type === "reasoning") return false
@@ -31,11 +44,26 @@ export function AssistantMessage(props: { message: AssistantMessage; parts: Part
)
}
-export function Part(props: { part: Part; message: Message; readonly?: boolean }) {
+function UserMessage(props: { message: UserMessage; parts: Part[] }) {
+ const text = createMemo(() =>
+ props.parts
+ ?.filter((p) => p.type === "text" && !p.synthetic)
+ ?.map((p) => (p as TextPart).text)
+ ?.join(""),
+ )
+ return {text()}
+}
+
+export function Part(props: { part: Part; message: Message; hideDetails?: boolean }) {
const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
return (
-
+
)
}
@@ -62,7 +90,7 @@ function TextPart(props: { part: TextPart; message: Message }) {
)
}
-function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean }) {
+function ToolPart(props: { part: ToolPart; message: Message; hideDetails?: boolean }) {
const component = createMemo(() => {
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
@@ -75,7 +103,7 @@ function ToolPart(props: { part: ToolPart; message: Message; readonly?: boolean
tool={props.part.tool}
metadata={metadata}
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
- readonly={props.readonly}
+ hideDetails={props.hideDetails}
/>
)
})
@@ -101,7 +129,7 @@ function BasicTool(props: {
icon: IconProps["name"]
trigger: TriggerTitle | JSX.Element
children?: JSX.Element
- readonly?: boolean
+ hideDetails?: boolean
}) {
const resolved = children(() => props.children)
return (
@@ -157,12 +185,12 @@ function BasicTool(props: {
-
+
-
+
{resolved()}
@@ -173,7 +201,7 @@ function BasicTool(props: {
}
function GenericTool(props: ToolProps) {
- return
+ return
}
type ToolProps = {
@@ -181,7 +209,7 @@ type ToolProps = {
metadata: Partial>
tool: string
output?: string
- readonly?: boolean
+ hideDetails?: boolean
}
const ToolRegistry = (() => {
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 800f3651e..ac6b6f9c8 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -33,7 +33,7 @@ import { Code } from "@/components/code"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { ProgressCircle } from "@/components/progress-circle"
-import { AssistantMessage, Part } from "@/components/assistant-message"
+import { Message, Part } from "@/components/message"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { DiffChanges } from "@/components/diff-changes"
@@ -198,6 +198,7 @@ export default function Page() {
}
if (!session) return
+ local.session.setActive(session.id)
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
const text = parts.map((part) => part.content).join("")
@@ -259,7 +260,6 @@ export default function Page() {
],
},
})
- local.session.setActive(session.id)
}
const handleNewSession = () => {
@@ -639,8 +639,9 @@ export default function Page() {
{(message) => {
const [expanded, setExpanded] = createSignal(false)
- const title = createMemo(() => message.summary?.title)
+ const parts = createMemo(() => sync.data.part[message.id])
const prompt = createMemo(() => local.session.getMessageText(message))
+ const title = createMemo(() => message.summary?.title)
const summary = createMemo(() => message.summary?.body)
const assistantMessages = createMemo(() => {
return sync.data.message[activeSession().id]?.filter(
@@ -665,7 +666,9 @@ export default function Page() {
- {prompt()}
+
+
+
{/* Response */}
@@ -686,7 +689,7 @@ export default function Page() {
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
- return
+ return
}}
@@ -722,7 +725,9 @@ export default function Page() {
const lastTextPart = createMemo(() =>
sync.data.part[last().id].findLast((p) => p.type === "text"),
)
- return
+ return (
+
+ )
}}
@@ -733,7 +738,11 @@ export default function Page() {
),
)
return (
-
+
)
}}
@@ -745,7 +754,7 @@ export default function Page() {
(p) => p.type === "tool" && p.state.status === "completed",
),
)
- return
+ return
}}
diff --git a/packages/function/package.json b/packages/function/package.json
index d5b390566..fabb7168a 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "0.15.24",
+ "version": "0.15.28",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index c180053c3..91c5dba94 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "0.15.24",
+ "version": "0.15.28",
"name": "opencode",
"type": "module",
"private": true,
@@ -22,6 +22,10 @@
"@ai-sdk/google-vertex": "3.0.16",
"@babel/core": "7.28.4",
"@octokit/webhooks-types": "7.6.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 15b40b14e..40d8dcd49 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -1506,6 +1506,9 @@ export namespace SessionPrompt {
})
export type CommandInput = z.infer
const bashRegex = /!`([^`]+)`/g
+ const argsRegex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g
+ const placeholderRegex = /\$(\d+)/g
+ const quoteTrimRegex = /^["']|["']$/g
/**
* Regular expression to match @ file references in text
* Matches @ followed by file paths, excluding commas, periods at end of sentences, and backticks
@@ -1517,7 +1520,25 @@ export namespace SessionPrompt {
const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? "build"
- let template = command.template.replaceAll("$ARGUMENTS", input.arguments)
+ const raw = input.arguments.match(argsRegex) ?? []
+ const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
+
+ const placeholders = command.template.match(placeholderRegex) ?? []
+ let last = 0
+ for (const item of placeholders) {
+ const value = Number(item.slice(1))
+ if (value > last) last = value
+ }
+
+ // Let the final placeholder swallow any extra arguments so prompts read naturally
+ const withArgs = command.template.replaceAll(placeholderRegex, (_, index) => {
+ const position = Number(index)
+ const argIndex = position - 1
+ if (argIndex >= args.length) return ""
+ if (position === last) return args.slice(argIndex).join(" ")
+ return args[argIndex]
+ })
+ let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
const shell = ConfigMarkdown.shell(template)
if (shell.length > 0) {
diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts
index 744465dcf..115fbe452 100644
--- a/packages/opencode/src/session/summary.ts
+++ b/packages/opencode/src/session/summary.ts
@@ -98,23 +98,29 @@ export namespace SessionSummary {
m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
)
) {
- const result = await generateText({
- model: small.language,
- maxOutputTokens: 100,
- messages: [
- {
- role: "user",
- content: `
+ let summary = messages
+ .findLast((m) => m.info.role === "assistant")
+ ?.parts.findLast((p) => p.type === "text")?.text
+ if (!summary || diffs.length > 0) {
+ const result = await generateText({
+ model: small.language,
+ maxOutputTokens: 100,
+ messages: [
+ {
+ role: "user",
+ content: `
Summarize the following conversation into 2 sentences MAX explaining what the assistant did and why. Do not explain the user's input. Do not speak in the third person about the assistant.
${JSON.stringify(MessageV2.toModelMessage(messages))}
`,
- },
- ],
- })
- userMsg.summary.body = result.text
- log.info("body", { body: result.text })
+ },
+ ],
+ })
+ summary = result.text
+ }
+ userMsg.summary.body = summary
+ log.info("body", { body: summary })
await Session.updateMessage(userMsg)
}
}
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 828484fbc..2409aa979 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
- "version": "0.15.24",
+ "version": "0.15.28",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 91e8051f2..ba6a1b8b7 100644
--- a/packages/sdk/js/package.json
+++ b/packages/sdk/js/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
- "version": "0.15.24",
+ "version": "0.15.28",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 3e9e857fd..e41e63cd9 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
- "version": "0.15.24",
+ "version": "0.15.28",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 16608105c..dcec177ba 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
- "version": "0.15.24",
+ "version": "0.15.28",
"type": "module",
"exports": {
".": "./src/components/index.ts",
diff --git a/packages/web/package.json b/packages/web/package.json
index a7242a1fa..0bd80e056 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
- "version": "0.15.24",
+ "version": "0.15.28",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
diff --git a/packages/web/src/content/docs/commands.mdx b/packages/web/src/content/docs/commands.mdx
index adfa3fbf3..463ad9e49 100644
--- a/packages/web/src/content/docs/commands.mdx
+++ b/packages/web/src/content/docs/commands.mdx
@@ -129,6 +129,36 @@ Run the command with arguments:
And `$ARGUMENTS` will be replaced with `Button`.
+You can also access individual arguments using positional parameters:
+
+- `$1` - First argument
+- `$2` - Second argument
+- `$3` - Third argument
+- And so on...
+
+For example:
+
+```md title=".opencode/command/create-file.md"
+---
+description: Create a new file with content
+---
+
+Create a file named $1 in the directory $2
+with the following content: $3
+```
+
+Run the command:
+
+```bash frame="none"
+/create-file config.json src "{ \"key\": \"value\" }"
+```
+
+This replaces:
+
+- `$1` with `config.json`
+- `$2` with `src`
+- `$3` with `{ "key": "value" }`
+
---
### Shell output
diff --git a/packages/web/src/content/docs/enterprise.mdx b/packages/web/src/content/docs/enterprise.mdx
index c8a67ec34..0899d4858 100644
--- a/packages/web/src/content/docs/enterprise.mdx
+++ b/packages/web/src/content/docs/enterprise.mdx
@@ -55,6 +55,45 @@ We recommend you disable this for your trial.
---
+## FAQ
+
+
+What is OpenCode Enterprise?
+
+OpenCode Enterprise provides self-hosted deployment options with enhanced security, SSO integration, and dedicated support for organizations that need to maintain full control over their development environment.
+
+
+
+
+How does enterprise pricing work?
+
+Enterprise pricing is based on team size and deployment requirements. Contact us at {config.email} for a custom quote based on your organization's needs.
+
+
+
+
+What deployment options are available?
+
+We offer cloud-hosted, on-premises, and air-gapped deployment options. Each includes SSO integration, private package registry support, and customizable security configurations.
+
+
+
+
+Is my data secure with enterprise?
+
+Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. Enterprise deployments add SSO protection and can be fully air-gapped for maximum security.
+
+
+
+
+Can we integrate with existing tools?
+
+Yes. OpenCode supports private npm registries, custom authentication providers, and can be integrated into your existing CI/CD pipelines and development workflows.
+
+
+
+---
+
## Deployment
Once you have completed your trial and you are ready to self-host opencode at
diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json
index bcb98bf66..bd671373f 100644
--- a/sdks/vscode/package.json
+++ b/sdks/vscode/package.json
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
- "version": "0.15.24",
+ "version": "0.15.28",
"publisher": "sst-dev",
"repository": {
"type": "git",