Compare commits

..

43 Commits

Author SHA1 Message Date
opencode 4b3a841dd9 release: v0.15.29 2025-10-30 06:49:34 +00:00
Aiden Cline aca32eaa1c fix: tui not showing some errors 2025-10-30 01:41:28 -05:00
Matt Gillard 3ae75d7031 add optional timeout field to mcp config to allow users to use responding servers (#3558)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-30 01:25:24 -05:00
Khang Ha (Kelvin) 4b5e447961 Fix "bufio.Scanner token too long" error by replacing Scanner with Reader in SSE (#3531) 2025-10-30 01:04:06 -05:00
Aiden Cline 7a2b8eae76 tweak: catch err 2025-10-30 00:29:30 -05:00
ElecTwix d983b9485d fix: add doom loop detection (#3445)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-30 00:13:18 -05:00
GitHub Action 14836de276 ignore: update download stats 2025-10-30 2025-10-30 02:28:13 +00:00
Jay V a0f032c9b9 ignore: fix email formatting to ensure proper line breaks between message and signature 2025-10-29 19:19:28 -04:00
Jay V e6132fc6a4 docs: enterprise 2025-10-29 19:07:43 -04:00
Frank 950b608c4d zen: show browser default time 2025-10-29 17:14:28 -04:00
Adam 3210df7428 wip: desktop work 2025-10-29 16:04:34 -05:00
Adam cdeb82e9ca wip: desktop work 2025-10-29 16:04:34 -05:00
rienkim a9cae7b335 feat: add positional argument support to slash commands (#3456)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-29 14:54:24 -05:00
Frank 972c0893dd zen: fix unified endpoint for codex 2025-10-29 15:06:37 -04:00
opencode e5d89ca567 release: v0.15.28 2025-10-29 19:00:56 +00:00
Aiden Cline 4ae70d4b0d fix: parcel watcher issue (#3544) 2025-10-29 13:52:38 -05:00
opencode 935cd7481b release: v0.15.27 2025-10-29 18:41:24 +00:00
Dax Raad 5553efea5e only generate user message summary if no diffs 2025-10-29 14:28:43 -04:00
Adam 0ff73ed8a6 wip: desktop work 2025-10-29 16:59:45 +00:00
opencode 5e792d7ac5 release: v0.15.26 2025-10-29 16:59:45 +00:00
David Hill 4a77e94e3c Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-29 16:16:56 +00:00
David Hill 4c563ea405 Enron update 2025-10-29 16:16:48 +00:00
Aiden Cline 5875257462 ignore: add label 2025-10-29 11:08:46 -05:00
David Hill 9701891e94 testimonial and other polish 2025-10-29 16:02:25 +00:00
opencode a2ab37c1b6 release: v0.15.25 2025-10-29 16:01:53 +00:00
Dax Raad 4d6e2d8efc autoupgrade latest major only 2025-10-29 11:53:25 -04:00
Aiden Cline 4407d5d96f fix: ensure tool inputs are zod validated 2025-10-29 10:46:57 -05:00
Adam 244945c0e7 fix: desktop error 2025-10-29 10:43:34 -05:00
opencode c652b2b4e8 release: v0.15.24 2025-10-29 15:38:55 +00:00
David Hill aabeeb1431 Adding polish 2025-10-29 15:34:45 +00:00
David Hill 0fbedc5e19 Fix form submission 2025-10-29 15:34:39 +00:00
Dax Raad 12782fff14 remove log 2025-10-29 11:27:51 -04:00
Dax Raad ca463a2346 session diff only include modified files 2025-10-29 11:26:21 -04:00
Aiden Cline 7265cdf817 ignore: rm 2025-10-29 10:19:02 -05:00
David Hill 7baa751351 First pass at adding an enterprise page 2025-10-29 15:16:17 +00:00
Adam 5b86fa9109 wip: desktop work 2025-10-29 07:32:01 -05:00
Adam aa7e008fe1 wip: desktop work 2025-10-29 07:32:00 -05:00
GitHub Action 792664071c ignore: update download stats 2025-10-29 2025-10-29 12:04:57 +00:00
Aiden Cline a0541ba57a zen: fix models endpoint to be openai compatible 2025-10-28 22:48:57 -05:00
Aiden Cline 4994bf1b46 ignore: rename type 2025-10-28 22:39:57 -05:00
Tyler Gannon 1e24514d61 add OpenAPI annotations to tui.ts control endpoints (#3519) 2025-10-28 22:39:22 -05:00
opencode 4b1c6300a0 release: v0.15.23 2025-10-29 01:35:27 +00:00
Dax Raad db3fb9d316 ci: stuff 2025-10-28 21:28:44 -04:00
64 changed files with 2595 additions and 437 deletions
+5 -1
View File
@@ -53,13 +53,17 @@ jobs:
- name: Install OpenCode
run: curl -fsSL https://opencode.ai/install | bash
- name: Setup npm auth
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+1
View File
@@ -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.
+2
View File
@@ -122,3 +122,5 @@
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
+15 -11
View File
@@ -37,7 +37,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -109,7 +109,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.15.20",
"version": "0.15.29",
"bin": {
"opencode": "./bin/opencode",
},
@@ -217,6 +217,10 @@
"@ai-sdk/google-vertex": "3.0.16",
"@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:",
@@ -230,7 +234,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -250,7 +254,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.15.20",
"version": "0.15.29",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -261,7 +265,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -274,7 +278,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "catalog:",
@@ -297,7 +301,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -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.20"
"version": "0.15.29"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -36,6 +36,9 @@ export function Header(props: { zen?: boolean }) {
<li>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
@@ -107,6 +110,9 @@ export function Header(props: { zen?: boolean }) {
<li>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
@@ -0,0 +1,46 @@
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 = `
${body.message}<br><br>
--
${body.name}<br>
${body.role}<br>
${body.email}`.trim()
// Send email using AWS SES
await AWS.sendEmail({
to: "contact@anoma.ly",
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 })
}
}
@@ -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;
}
}
}
@@ -0,0 +1,267 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { createSignal, Show } 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 (
<main data-page="enterprise">
<Title>OpenCode | Enterprise solutions for your organisation</Title>
<Meta name="description" content="Contact OpenCode for enterprise solutions" />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="enterprise-content">
<div data-component="enterprise-columns">
<div data-component="enterprise-column-1">
<h2>Your code is yours</h2>
<p>
OpenCode operates securely inside your organization with no data or context stored
and no licensing restrictions or ownership claims. Start a trial with your team
, then deploy it across your organization by integrating it with your SSO and internal AI gateway.
</p>
<p>Let us know and how we can help.</p>
<Show when={false}>
<div data-component="testimonial">
<div data-component="quotation">
<svg
width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor"
/>
</svg>
</div>
Thanks to OpenCode, we found a way to create software to track all our assets
even the imaginary ones.
<div data-component="testimonial-logo">
<svg
width="80"
height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 39.3087L10.0579 29.251L15.6862 34.7868L13.7488 36.7248L10.3345 33.2186L8.48897 35.0639L11.8111 38.4781L9.96557 40.4156L6.55181 37.0018L4.06028 39.4928L7.56674 42.9991L5.62884 44.845L0 39.3087Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.7182 36.8164L20.2094 39.4003L16.6108 46.9666L22.2393 41.3374L24.3615 43.46L14.2118 53.5179L11.9047 51.1187L15.4112 43.3677L9.78254 49.0888L7.66016 46.9666L17.7182 36.8164Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M42.8139 61.915L45.3055 64.4064L41.6145 71.9731L47.243 66.3441L49.3652 68.4663L39.3077 78.5244L36.9088 76.1252L40.5072 68.374L34.7866 74.0953L32.6641 71.9731L42.8139 61.915Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.4258 55.7324L26.4833 45.582L28.6061 47.7042C31.0049 50.1034 32.3892 51.9497 30.1746 54.1642C28.7902 55.548 27.6831 56.0094 26.1145 54.9016L26.0222 54.994C27.2218 56.1941 26.9448 57.1162 25.4688 58.5931L23.9 60.1615C23.4383 60.6232 22.8847 61.2693 22.7927 62.0067L20.6705 59.8845C20.7625 59.146 21.3161 58.5008 21.778 58.1316L23.5307 56.3788C24.269 55.6403 23.715 54.2555 23.254 53.8872L22.8847 53.4256L18.548 57.7623L16.4258 55.7324ZM24.3611 51.9495C25.4689 53.0563 26.4833 53.3332 27.4984 52.3178C28.5134 51.3957 28.2367 50.3802 27.1295 49.1812L24.3611 51.9495Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M33.4952 66.9899C31.096 69.3891 28.8815 68.4659 27.4047 66.9899C26.021 65.6062 25.0978 63.3907 27.4972 60.9003L31.8336 56.6548C34.2333 54.2556 36.4478 55.0864 37.9241 56.5635C39.308 58.0396 40.2311 60.2541 37.8315 62.6531L33.4952 66.9899ZM29.0659 63.5752C28.6048 64.0369 28.6048 64.7753 29.1583 65.3292C29.6196 65.8821 30.4502 65.7897 30.8194 65.4215L36.2633 59.9769C36.7246 59.6076 36.7246 58.7779 36.171 58.3164C35.7097 57.7626 34.8791 57.7626 34.5101 58.2241L29.0659 63.5752Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M78.5267 39.308L68.2845 29.0654L47.5231 49.735L49.6453 51.8572L68.2845 33.2179L74.3746 39.308L47.2461 66.3435L49.3683 68.4657L78.5267 39.308Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M49.6443 51.8577L43.3695 45.4902L64.0386 24.8215L53.7969 14.4873L33.0352 35.2482L35.1574 37.3705L53.7969 18.7315L59.7947 24.8215L39.1251 45.4902L47.5221 53.9799L49.6443 51.8577Z"
fill="#2D9C5C"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M35.1564 37.3706L28.7896 31.0038L49.5515 10.3347L39.3088 0L10.0586 29.2507L12.1804 31.2804L39.3088 4.24476L45.3066 10.3347L24.6377 31.0038L33.0342 39.4008L35.1564 37.3706Z"
fill="#E92A35"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M77.2332 52.4105C76.0336 52.4105 75.111 51.4884 75.111 50.196C75.111 48.9046 76.0336 47.9814 77.2332 47.9814C78.3405 47.9814 79.263 48.9046 79.263 50.196C79.263 51.4884 78.3405 52.4105 77.2332 52.4105ZM77.2332 52.9643C78.7098 52.9643 80.0015 51.6729 80.0015 50.196C80.0015 48.6276 78.7096 47.4287 77.2332 47.4287C75.6644 47.4287 74.4648 48.6278 74.4648 50.196C74.4647 51.6731 75.6643 52.9643 77.2332 52.9643ZM76.1259 51.7653H76.6797V50.3804H77.0485L77.8788 51.7653H78.4332L77.6023 50.3804C78.1558 50.2881 78.4332 50.0122 78.4332 49.5507C78.4332 48.9046 78.0633 48.6276 77.3253 48.6276H76.1257V51.7653H76.1259ZM76.6797 49.0892H77.2332C77.5102 49.0892 77.8788 49.0892 77.8788 49.4586C77.8788 49.9202 77.6023 49.9202 77.2332 49.9202H76.6797V49.0892Z"
fill="#0083C6"
/>
</svg>
</div>
</div>
</Show>
</div>
<div data-component="enterprise-column-2">
<div data-component="enterprise-form">
<form onSubmit={handleSubmit}>
<div data-component="form-group">
<label for="name">Full name</label>
<input
id="name"
type="text"
required
value={formData().name}
onInput={handleInputChange("name")}
placeholder="Jeff Bezos"
/>
</div>
<div data-component="form-group">
<label for="role">Role</label>
<input
id="role"
type="text"
required
value={formData().role}
onInput={handleInputChange("role")}
placeholder="Executive Chairman"
/>
</div>
<div data-component="form-group">
<label for="email">Company email</label>
<input
id="email"
type="email"
required
value={formData().email}
onInput={handleInputChange("email")}
placeholder="jeff@amazon.com"
/>
</div>
<div data-component="form-group">
<label for="message">What problem are you trying to solve?</label>
<textarea
id="message"
required
rows={5}
value={formData().message}
onInput={handleInputChange("message")}
placeholder="We need help with..."
/>
</div>
<button type="submit" disabled={isSubmitting()} data-component="submit-button">
{isSubmitting() ? "Sending..." : "Send"}
</button>
</form>
{showSuccess() && (
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
</div>
</div>
</div>
</section>
<section data-component="faq">
<div data-slot="section-title">
<h3>FAQ</h3>
</div>
<ul>
<li>
<Faq question="What is OpenCode Enterprise?">
OpenCode Enterprise is for organizations that want to ensure that their code and
data never leaves their infrastructure. It can do this by using a centralized
config that integrates with your SSO and internal AI gateway.
</Faq>
</li>
<li>
<Faq question="How do I get started with OpenCode Enterprise?">
Simply start with an internal trial with your team. OpenCode by default does not
store your code or context data, making it easy to get started. Then contact us to
discuss pricing and implementation options.
</Faq>
</li>
<li>
<Faq question="How does enterprise pricing work?">
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not
charge for tokens used. For further details, contact us for a custom quote based
on your organization's needs.
</Faq>
</li>
<li>
<Faq question="Is my data secure with OpenCode Enterprise?">
Yes. OpenCode does not store your code or context data. All processing happens
locally or through direct API calls to your AI provider. With central config and
SSO integration, your data remains secure within your organization's
infrastructure.
</Faq>
</li>
</ul>
</section>
</div>
<Footer />
</div>
<Legal />
</main>
)
}
+1 -5
View File
@@ -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;
@@ -16,7 +16,7 @@ export function formatDateForTable(date: Date) {
minute: "2-digit",
hour12: true,
}
return date.toLocaleDateString("en-GB", options).replace(",", ",")
return date.toLocaleDateString(undefined, options).replace(",", ",")
}
export function formatDateUTC(date: Date) {
@@ -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,
@@ -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,
@@ -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" },
}
}
@@ -95,7 +95,7 @@ export interface CommonUsage {
}
export interface CommonRequest {
model?: string
model: string
max_tokens?: number
temperature?: number
top_p?: number
@@ -25,8 +25,8 @@ export async function GET(input: APIEvent) {
object: "list",
data: Object.entries(zenData.models)
.filter(([id]) => !disabledModels.includes(id))
.map(([id, model]) => ({
id: `opencode/${id}`,
.map(([id, _model]) => ({
id,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "opencode",
@@ -50,7 +50,10 @@ export async function GET(input: APIEvent) {
})
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows.map((row) => row.model)),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.15.20",
"version": "0.15.29",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "0.15.20",
"version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.15.20",
"version": "0.15.29",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "0.15.20",
"version": "0.15.29",
"description": "",
"type": "module",
"scripts": {
@@ -1,4 +1,4 @@
import type { Part, AssistantMessage, ReasoningPart, TextPart, ToolPart } 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,47 +17,64 @@ 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[]; lastToolOnly?: boolean }) {
export function Message(props: { message: Message; parts: Part[] }) {
return (
<Switch>
<Match when={props.message.role === "user" && props.message}>
{(userMessage) => <UserMessage message={userMessage()} parts={props.parts} />}
</Match>
<Match when={props.message.role === "assistant" && props.message}>
{(assistantMessage) => <AssistantMessage message={assistantMessage()} parts={props.parts} />}
</Match>
</Switch>
)
}
function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
const filteredParts = createMemo(() => {
let tool = false
return props.parts?.filter((x) => {
if (x.type === "tool" && props.lastToolOnly && tool) return false
if (x.type === "tool") tool = true
if (x.type === "reasoning") return false
return x.type !== "tool" || x.tool !== "todoread"
})
})
return (
<div class="w-full flex flex-col items-start gap-4">
<For each={filteredParts()}>
{(part) => {
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
return (
<Show when={component()}>
<Dynamic component={component()} part={part as any} message={props.message} />
</Show>
)
}}
</For>
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
</div>
)
}
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 <div class="text-12-regular text-text-base line-clamp-3">{text()}</div>
}
export function Part(props: { part: Part; message: Message; hideDetails?: boolean }) {
const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
return (
<Show when={component()}>
<Dynamic
component={component()}
part={props.part as any}
message={props.message}
hideDetails={props.hideDetails}
/>
</Show>
)
}
const PART_MAPPING = {
text: TextPart,
tool: ToolPart,
reasoning: ReasoningPart,
}
function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
return null
// return (
// <Show when={props.part.text.trim()}>
// <div>{props.part.text}</div>
// </Show>
// )
}
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
function ReasoningPart(props: { part: ReasoningPart; message: Message }) {
return (
<Show when={props.part.text.trim()}>
<Markdown text={props.part.text.trim()} />
@@ -65,17 +82,19 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) {
)
}
function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
// const sync = useSync()
function TextPart(props: { part: TextPart; message: Message }) {
return (
<Show when={props.part.text.trim()}>
<Markdown text={props.part.text.trim()} />
</Show>
)
}
function ToolPart(props: { part: ToolPart; message: Message; hideDetails?: boolean }) {
const component = createMemo(() => {
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
const input = props.part.state.status === "completed" ? props.part.state.input : {}
// const permissions = sync.data.permission[props.message.sessionID] ?? []
// const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
// const permission = permissions[permissionIndex]
return (
<Dynamic
@@ -83,8 +102,8 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
input={input}
tool={props.part.tool}
metadata={metadata}
// permission={permission?.metadata ?? {}}
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
hideDetails={props.hideDetails}
/>
)
})
@@ -106,7 +125,12 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
}
function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX.Element; children?: JSX.Element }) {
function BasicTool(props: {
icon: IconProps["name"]
trigger: TriggerTitle | JSX.Element
children?: JSX.Element
hideDetails?: boolean
}) {
const resolved = children(() => props.children)
return (
<Collapsible>
@@ -161,13 +185,13 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
</Switch>
</div>
</div>
<Show when={resolved()}>
<Show when={resolved() && !props.hideDetails}>
<Collapsible.Arrow />
</Show>
</div>
</Collapsible.Trigger>
<Show when={props.children}>
<Collapsible.Content>{props.children}</Collapsible.Content>
<Show when={resolved() && !props.hideDetails}>
<Collapsible.Content>{resolved()}</Collapsible.Content>
</Show>
</Collapsible>
// <>
@@ -177,15 +201,15 @@ function BasicTool(props: { icon: IconProps["name"]; trigger: TriggerTitle | JSX
}
function GenericTool(props: ToolProps<any>) {
return <BasicTool icon="mcp" trigger={{ title: props.tool }} />
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
}
type ToolProps<T extends Tool.Info> = {
input: Partial<Tool.InferParameters<T>>
metadata: Partial<Tool.InferMetadata<T>>
// permission: Record<string, any>
tool: string
output?: string
hideDetails?: boolean
}
const ToolRegistry = (() => {
@@ -393,9 +393,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
<Show when={i.release_date}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>
</div>
<Show when={!i.cost || i.cost?.input === 0}>
-1
View File
@@ -481,7 +481,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!message) return ""
if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
console.log(fileParts)
return sync.data.part[message.id]
?.filter((p) => p.type === "text")
+140 -58
View File
@@ -33,9 +33,10 @@ import { Code } from "@/components/code"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { ProgressCircle } from "@/components/progress-circle"
import { AssistantMessage } 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"
import { Markdown } from "@/components/markdown"
export default function Page() {
const local = useLocal()
@@ -178,6 +179,8 @@ export default function Page() {
}
const handleDiffTriggerClick = (event: MouseEvent) => {
// disabling scroll to diff for now
return
const target = event.currentTarget as HTMLElement
queueMicrotask(() => {
if (target.getAttribute("aria-expanded") !== "true") return
@@ -196,6 +199,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("")
@@ -257,7 +261,6 @@ export default function Page() {
],
},
})
local.session.setActive(session.id)
}
const handleNewSession = () => {
@@ -489,8 +492,8 @@ export default function Page() {
</Show>
</div>
</div>
<Tabs.Content value="chat" class="select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div class="px-6 pt-12 max-w-[904px] w-full mx-auto flex flex-col flex-1 min-h-0">
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div class="relative px-6 pt-12 max-w-2xl w-full mx-auto flex flex-col flex-1 min-h-0">
<Show
when={local.session.active()}
fallback={
@@ -517,9 +520,12 @@ export default function Page() {
>
{(activeSession) => (
<div class="pt-3 flex flex-col flex-1 min-h-0">
<div class="flex items-start gap-8 flex-1 min-h-0">
<div class="flex-1 min-h-0">
<Show when={local.session.userMessages().length > 1}>
<ul role="list" class="w-60 shrink-0 flex flex-col items-start gap-1">
<ul
role="list"
class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1"
>
<For each={local.session.userMessages()}>
{(message) => {
const countLines = (text: string) => {
@@ -636,28 +642,23 @@ export default function Page() {
<div class="flex flex-col items-start gap-50 pb-50">
<For each={local.session.userMessages()}>
{(message) => {
const title = createMemo(() => message.summary?.title)
const [expanded, setExpanded] = createSignal(false)
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(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const working = createMemo(() => {
const last = assistantMessages()[assistantMessages().length - 1]
if (!last) return false
return !last.time.completed
})
const lastWithContent = createMemo(() =>
assistantMessages().findLast((m) => {
const parts = sync.data.part[m.id]
return parts?.find((p) => p.type === "text" || p.type === "tool")
}),
)
const working = createMemo(() => !summary())
return (
<div data-message={message.id} class="flex flex-col items-start self-stretch gap-8">
<div
data-message={message.id}
class="flex flex-col items-start self-stretch gap-8 min-h-screen"
>
{/* Title */}
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
@@ -665,43 +666,18 @@ export default function Page() {
</h1>
</div>
<Show when={title}>
<div class="-mt-5 text-12-regular text-text-base line-clamp-3">{prompt()}</div>
<div class="-mt-8">
<Message message={message} parts={parts()} />
</div>
</Show>
{/* Response */}
<div class="w-full flex flex-col gap-2">
<Collapsible variant="ghost">
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch">
<h2 class="text-12-medium">Show steps</h2>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="w-full flex flex-col items-start self-stretch gap-8">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
return <AssistantMessage message={assistantMessage} parts={parts()} />
}}
</For>
</div>
</Collapsible.Content>
</Collapsible>
<Show when={working() && lastWithContent()}>
{(last) => {
const lastParts = createMemo(() => sync.data.part[last().id])
return (
<AssistantMessage lastToolOnly message={last()} parts={lastParts()} />
)
}}
</Show>
</div>
{/* Summary */}
<Show when={!working()}>
<div class="w-full flex flex-col gap-6 items-start self-stretch">
<div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak">Summary</h2>
<div class="text-14-regular text-text-base self-stretch">{summary()}</div>
<Show when={summary()}>
<Markdown text={summary()!} />
</Show>
</div>
<Accordion class="w-full" multiple>
<For each={message.summary?.diffs || []}>
@@ -751,6 +727,119 @@ export default function Page() {
</Accordion>
</div>
</Show>
{/* Response */}
<div class="w-full">
<Switch>
<Match when={working()}>
{(_) => {
const items = createMemo(() =>
assistantMessages().flatMap((m) => sync.data.part[m.id]),
)
const finishedItems = createMemo(() =>
items().filter(
(p) =>
(p?.type === "text" && p.time?.end) ||
(p?.type === "reasoning" && p.time?.end) ||
(p?.type === "tool" && p.state.status === "completed"),
),
)
const MINIMUM_DELAY = 800
const [visibleCount, setVisibleCount] = createSignal(1)
createEffect(() => {
const total = finishedItems().length
if (total > visibleCount()) {
const timer = setTimeout(() => {
setVisibleCount((prev) => prev + 1)
}, MINIMUM_DELAY)
onCleanup(() => clearTimeout(timer))
} else if (total < visibleCount()) {
setVisibleCount(total)
}
})
const translateY = createMemo(() => {
const total = visibleCount()
if (total < 2) return "0px"
return `-${(total - 2) * 48 - 8}px`
})
return (
<div class="flex flex-col gap-3">
<div
class="h-36 overflow-hidden pointer-events-none
mask-alpha mask-y-from-66% mask-y-from-background-base mask-y-to-transparent"
>
<div
class="w-full flex flex-col items-start self-stretch gap-2 py-10
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
style={{ transform: `translateY(${translateY()})` }}
>
<For each={finishedItems()}>
{(part) => {
const message = createMemo(() =>
sync.data.message[part.sessionID].find(
(m) => m.id === part.messageID,
),
)
return (
<div class="h-10 flex items-center w-full">
<Switch>
<Match when={part.type === "text" && part}>
{(p) => (
<div
textContent={p().text}
class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
/>
)}
</Match>
<Match when={part.type === "reasoning" && part}>
{(p) => <Part message={message()!} part={p()} />}
</Match>
<Match when={part.type === "tool" && part}>
{(p) => <Part message={message()!} part={p()} />}
</Match>
</Switch>
</div>
)
}}
</For>
</div>
</div>
</div>
)
}}
</Match>
<Match when={!working()}>
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch">
<div class="text-12-medium">
<Switch>
<Match when={expanded()}>Hide steps</Match>
<Match when={!expanded()}>Show steps</Match>
</Switch>
</div>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="w-full flex flex-col items-start self-stretch gap-8">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(
() => sync.data.part[assistantMessage.id],
)
return <Message message={assistantMessage} parts={parts()} />
}}
</For>
</div>
</Collapsible.Content>
</Collapsible>
</Match>
</Switch>
</div>
</div>
)
}}
@@ -791,14 +880,7 @@ export default function Page() {
})()}
</DragOverlay>
</DragDropProvider>
<div
classList={{
"absolute inset-x-0 px-6 max-w-[904px] flex flex-col justify-center items-center z-50 mx-auto": true,
"bottom-8": true,
// "bottom-8": !!local.session.active(),
// "bottom-1/2 translate-y-1/2": !local.session.active(),
}}
>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "0.15.20",
"version": "0.15.29",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+5 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.15.20",
"version": "0.15.29",
"name": "opencode",
"type": "module",
"private": true,
@@ -20,6 +20,10 @@
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/google-vertex": "3.0.16",
"@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:",
+196 -38
View File
@@ -12,7 +12,11 @@ import { NamedError } from "../util/error"
import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
import {
type ParseError as JsoncParseError,
parse as parseJsonc,
printParseErrorCode,
} from "jsonc-parser"
import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { BunProc } from "@/bun"
@@ -46,7 +50,10 @@ export namespace Config {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
result = mergeDeep(
result,
await load(JSON.stringify(wellknown.config ?? {}), process.cwd()),
)
}
}
@@ -57,7 +64,11 @@ export namespace Config {
const directories = [
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({ targets: [".opencode"], start: Instance.directory, stop: Instance.worktree }),
Filesystem.up({
targets: [".opencode"],
start: Instance.directory,
stop: Instance.worktree,
}),
)),
]
@@ -153,10 +164,18 @@ export namespace Config {
const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
if (!hasGitIgnore)
await Bun.write(
gitignore,
["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"),
)
await BunProc.run(
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
[
"add",
"@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION),
"--exact",
],
{
cwd: dir,
},
@@ -166,7 +185,12 @@ export namespace Config {
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
async function loadCommand(dir: string) {
const result: Record<string, Command> = {}
for await (const item of COMMAND_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
for await (const item of COMMAND_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -201,7 +225,12 @@ export namespace Config {
async function loadAgent(dir: string) {
const result: Record<string, Agent> = {}
for await (const item of AGENT_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
for await (const item of AGENT_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -239,7 +268,12 @@ export namespace Config {
const MODE_GLOB = new Bun.Glob("mode/*.md")
async function loadMode(dir: string) {
const result: Record<string, Agent> = {}
for await (const item of MODE_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
for await (const item of MODE_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
@@ -265,7 +299,12 @@ export namespace Config {
async function loadPlugin(dir: string) {
const plugins: string[] = []
for await (const item of PLUGIN_GLOB.scan({ absolute: true, followSymlinks: true, dot: true, cwd: dir })) {
for await (const item of PLUGIN_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
cwd: dir,
})) {
plugins.push("file://" + item)
}
return plugins
@@ -280,6 +319,14 @@ export namespace Config {
.optional()
.describe("Environment variables to set when running the MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
timeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
),
})
.strict()
.meta({
@@ -291,7 +338,18 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
headers: z
.record(z.string(), z.string())
.optional()
.describe("Headers to send with the request"),
timeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
),
})
.strict()
.meta({
@@ -339,72 +397,148 @@ export namespace Config {
export const Keybinds = z
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
leader: z
.string()
.optional()
.default("ctrl+x")
.describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
thinking_blocks: z.string().optional().default("<leader>b").describe("Toggle thinking blocks"),
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
thinking_blocks: z
.string()
.optional()
.default("<leader>b")
.describe("Toggle thinking blocks"),
session_export: z
.string()
.optional()
.default("<leader>x")
.describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
session_timeline: z
.string()
.optional()
.default("<leader>g")
.describe("Show session timeline"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
session_child_cycle: z.string().optional().default("ctrl+right").describe("Cycle to next child session"),
session_child_cycle: z
.string()
.optional()
.default("ctrl+right")
.describe("Cycle to next child session"),
session_child_cycle_reverse: z
.string()
.optional()
.default("ctrl+left")
.describe("Cycle to previous child session"),
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
messages_page_up: z
.string()
.optional()
.default("pgup")
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.default("pgdown")
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.default("ctrl+alt+u")
.describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.default("ctrl+alt+d")
.describe("Scroll messages down by half page"),
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
messages_last: z
.string()
.optional()
.default("ctrl+alt+g")
.describe("Navigate to last message"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
model_list: z.string().optional().default("<leader>m").describe("List available models"),
model_cycle_recent: z.string().optional().default("f2").describe("Next recent model"),
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recent model"),
model_cycle_recent_reverse: z
.string()
.optional()
.default("shift+f2")
.describe("Previous recent model"),
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("enter").describe("Submit input"),
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
input_newline: z
.string()
.optional()
.default("shift+enter,ctrl+j")
.describe("Insert newline in input"),
// Deprecated commands
switch_mode: z.string().optional().default("none").describe("@deprecated use agent_cycle. Next mode"),
switch_mode: z
.string()
.optional()
.default("none")
.describe("@deprecated use agent_cycle. Next mode"),
switch_mode_reverse: z
.string()
.optional()
.default("none")
.describe("@deprecated use agent_cycle_reverse. Previous mode"),
switch_agent: z.string().optional().default("tab").describe("@deprecated use agent_cycle. Next agent"),
switch_agent: z
.string()
.optional()
.default("tab")
.describe("@deprecated use agent_cycle. Next agent"),
switch_agent_reverse: z
.string()
.optional()
.default("shift+tab")
.describe("@deprecated use agent_cycle_reverse. Previous agent"),
file_list: z.string().optional().default("none").describe("@deprecated Currently not available. List files"),
file_list: z
.string()
.optional()
.default("none")
.describe("@deprecated Currently not available. List files"),
file_close: z.string().optional().default("none").describe("@deprecated Close file"),
file_search: z.string().optional().default("none").describe("@deprecated Search file"),
file_diff_toggle: z.string().optional().default("none").describe("@deprecated Split/unified diff"),
messages_previous: z.string().optional().default("none").describe("@deprecated Navigate to previous message"),
messages_next: z.string().optional().default("none").describe("@deprecated Navigate to next message"),
messages_layout_toggle: z.string().optional().default("none").describe("@deprecated Toggle layout"),
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
file_diff_toggle: z
.string()
.optional()
.default("none")
.describe("@deprecated Split/unified diff"),
messages_previous: z
.string()
.optional()
.default("none")
.describe("@deprecated Navigate to previous message"),
messages_next: z
.string()
.optional()
.default("none")
.describe("@deprecated Navigate to next message"),
messages_layout_toggle: z
.string()
.optional()
.default("none")
.describe("@deprecated Toggle layout"),
messages_revert: z
.string()
.optional()
.default("none")
.describe("@deprecated use messages_undo. Revert message"),
})
.strict()
.meta({
@@ -446,13 +580,23 @@ export namespace Config {
autoshare: z
.boolean()
.optional()
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
.describe(
"@deprecated Use 'share' field instead. Share newly created sessions automatically",
),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
disabled_providers: z
.array(z.string())
.optional()
.describe("Disable providers that are loaded automatically"),
model: z
.string()
.describe("Model to use in the format of provider/model, eg anthropic/claude-2")
.optional(),
small_model: z
.string()
.describe("Small model to use for tasks like title generation in the format of provider/model")
.describe(
"Small model to use for tasks like title generation in the format of provider/model",
)
.optional(),
username: z
.string()
@@ -508,7 +652,10 @@ export namespace Config {
)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
mcp: z
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.record(
z.string(),
@@ -552,7 +699,10 @@ export namespace Config {
error: "For custom LSP servers, 'extensions' array is required.",
},
),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
instructions: z
.array(z.string())
.optional()
.describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z
.object({
@@ -586,7 +736,10 @@ export namespace Config {
.optional(),
})
.optional(),
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
chatMaxRetries: z
.number()
.optional()
.describe("Number of retries for chat completions on failure"),
disable_paste_summary: z.boolean().optional(),
})
.optional(),
@@ -616,7 +769,10 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await Bun.write(
path.join(Global.Path.config, "config.json"),
JSON.stringify(result, null, 2),
)
await fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
@@ -655,7 +811,9 @@ export namespace Config {
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const resolvedPath = path.isAbsolute(filePath)
? filePath
: path.resolve(configDir, filePath)
const fileContent = (
await Bun.file(resolvedPath)
.text()
+3 -1
View File
@@ -142,7 +142,9 @@ export namespace Installation {
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
export async function latest() {
return fetch(`https://registry.npmjs.org/opencode-ai/${CHANNEL}`)
const [major] = VERSION.split(".").map((x) => Number(x))
const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
+1 -1
View File
@@ -167,7 +167,7 @@ export namespace MCP {
return
}
const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {})
const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => { })
if (!result) {
log.warn("mcp client verification failed, dropping client", { name })
return
+53 -14
View File
@@ -1,12 +1,16 @@
import { Hono, type Context } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import { z } from "zod"
import { AsyncQueue } from "../util/queue"
interface Request {
path: string
body: any
}
const TuiRequest = z.object({
path: z.string(),
body: z.any(),
})
const request = new AsyncQueue<Request>()
type TuiRequest = z.infer<typeof TuiRequest>
const request = new AsyncQueue<TuiRequest>()
const response = new AsyncQueue<any>()
export async function callTui(ctx: Context) {
@@ -19,12 +23,47 @@ export async function callTui(ctx: Context) {
}
export const TuiRoute = new Hono()
.get("/next", async (c) => {
const req = await request.next()
return c.json(req)
})
.post("/response", async (c) => {
const body = await c.req.json()
response.push(body)
return c.json(true)
})
.get(
"/next",
describeRoute({
description: "Get the next TUI request from the queue",
operationId: "tui.control.next",
responses: {
200: {
description: "Next TUI request",
content: {
"application/json": {
schema: resolver(TuiRequest),
},
},
},
},
}),
async (c) => {
const req = await request.next()
return c.json(req)
},
)
.post(
"/response",
describeRoute({
description: "Submit a response to the TUI request queue",
operationId: "tui.control.response",
responses: {
200: {
description: "Response submitted successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
validator("json", z.any()),
async (c) => {
const body = c.req.valid("json")
response.push(body)
return c.json(true)
},
)
+103 -32
View File
@@ -56,6 +56,7 @@ export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = 32_000
const MAX_RETRIES = 10
const DOOM_LOOP_THRESHOLD = 3
export const Event = {
Idle: Bus.event(
@@ -286,7 +287,11 @@ export namespace SessionPrompt {
OUTPUT_TOKEN_MAX,
),
abortSignal: abort.signal,
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
providerOptions: ProviderTransform.providerOptions(
model.npm,
model.providerID,
params.options,
),
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
@@ -321,7 +326,11 @@ export namespace SessionPrompt {
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
args.params.prompt = ProviderTransform.message(
args.params.prompt,
model.providerID,
model.modelID,
)
}
return args.params
},
@@ -352,21 +361,21 @@ export namespace SessionPrompt {
const stop = await SessionRetry.sleep(delayMs, abort.signal)
.then(() => false)
.catch((error) => {
let err = error
if (error instanceof DOMException && error.name === "AbortError") {
const err = new MessageV2.AbortedError(
err = new MessageV2.AbortedError(
{ message: error.message },
{
cause: error,
},
).toObject()
result.info.error = err
Bus.publish(Session.Event.Error, {
sessionID: result.info.sessionID,
error: result.info.error,
})
return true
}
throw error
result.info.error = err
Bus.publish(Session.Event.Error, {
sessionID: result.info.sessionID,
error: result.info.error,
})
return true
})
if (stop) break
@@ -504,7 +513,11 @@ export namespace SessionPrompt {
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
const schema = ProviderTransform.schema(input.providerID, input.modelID, z.toJSONSchema(item.parameters))
const schema = ProviderTransform.schema(
input.providerID,
input.modelID,
z.toJSONSchema(item.parameters),
)
tools[item.id] = tool({
id: item.id as any,
description: item.description,
@@ -521,6 +534,7 @@ export namespace SessionPrompt {
args,
},
)
item.parameters.parse(args)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: options.abortSignal!,
@@ -585,17 +599,7 @@ export namespace SessionPrompt {
args,
},
)
const result = await execute(args, opts).catch((err: unknown) => {
log.error("Error executing tool", { error: err, tool: key })
return {
content: [
{
type: "text",
text: `Failed to execute tool: ${err instanceof Error ? err.message : String(err)}`,
},
],
}
})
const result = await execute(args, opts)
await Plugin.trigger(
"tool.execute.after",
@@ -809,7 +813,9 @@ export namespace SessionPrompt {
messageID: info.id,
sessionID: input.sessionID,
type: "file",
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
url:
`data:${part.mime};base64,` +
Buffer.from(await file.bytes()).toString("base64"),
mime: part.mime,
filename: part.filename!,
source: part.source,
@@ -883,7 +889,9 @@ export namespace SessionPrompt {
synthetic: true,
})
}
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan")
const wasPlan = input.messages.some(
(msg) => msg.info.role === "assistant" && msg.info.mode === "plan",
)
if (wasPlan && input.agent.name === "build") {
userMessage.parts.push({
id: Identifier.ascending("part"),
@@ -963,7 +971,10 @@ export namespace SessionPrompt {
partFromToolCall(toolCallID: string) {
return toolcalls[toolCallID]
},
async process(stream: StreamTextResult<Record<string, AITool>, never>, retries: { count: number; max: number }) {
async process(
stream: StreamTextResult<Record<string, AITool>, never>,
retries: { count: number; max: number },
) {
log.info("process")
if (!assistantMsg) throw new Error("call next() first before processing")
let shouldRetry = false
@@ -1058,6 +1069,32 @@ export namespace SessionPrompt {
metadata: value.providerMetadata,
})
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
const parts = await Session.getParts(assistantMsg.id)
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
(p) =>
p.type === "tool" &&
p.tool === value.toolName &&
p.state.status !== "pending" &&
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await Permission.ask({
type: "doom-loop",
pattern: value.toolName,
sessionID: assistantMsg.sessionID,
messageID: assistantMsg.id,
callID: value.toolCallId,
title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
metadata: {
tool: value.toolName,
input: value.input,
},
})
}
}
break
}
@@ -1094,7 +1131,10 @@ export namespace SessionPrompt {
status: "error",
input: value.input,
error: (value.error as any).toString(),
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
metadata:
value.error instanceof Permission.RejectedError
? value.error.metadata
: undefined,
time: {
start: match.state.time.start,
end: Date.now(),
@@ -1218,7 +1258,11 @@ export namespace SessionPrompt {
error: e,
})
const error = MessageV2.fromError(e, { providerID: input.providerID })
if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
if (
retries.count < retries.max &&
MessageV2.APIError.isInstance(error) &&
error.data.isRetryable
) {
shouldRetry = true
await Session.updatePart({
id: Identifier.ascending("part"),
@@ -1241,7 +1285,11 @@ export namespace SessionPrompt {
}
const p = await Session.getParts(assistantMsg.id)
for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
if (
part.type === "tool" &&
part.state.status !== "completed" &&
part.state.status !== "error"
) {
Session.updatePart({
...part,
state: {
@@ -1483,6 +1531,9 @@ export namespace SessionPrompt {
})
export type CommandInput = z.infer<typeof CommandInput>
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
@@ -1494,7 +1545,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) {
@@ -1705,11 +1774,13 @@ export namespace SessionPrompt {
if (input.session.parentID) return
if (!Session.isDefaultTitle(input.session.title)) return
const isFirst =
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
.length === 1
input.history.filter(
(m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
).length === 1
if (!isFirst) return
const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
(await Provider.getSmallModel(input.providerID)) ??
(await Provider.getModel(input.providerID, input.modelID))
const options = {
...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
...small.info.options,
+40 -18
View File
@@ -29,7 +29,15 @@ export namespace SessionSummary {
)
async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
const diffs = await computeDiff({ messages: input.messages })
const files = new Set(
input.messages
.flatMap((x) => x.parts)
.filter((x) => x.type === "patch")
.flatMap((x) => x.files),
)
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
x.filter((x) => files.has(x.file)),
)
await Session.update(input.sessionID, (draft) => {
draft.summary = {
diffs,
@@ -39,7 +47,9 @@ export namespace SessionSummary {
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
const messages = input.messages.filter(
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
(m) =>
m.info.id === input.messageID ||
(m.info.role === "assistant" && m.info.parentID === input.messageID),
)
const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
const userMsg = msgWithParts.info as MessageV2.User
@@ -50,11 +60,14 @@ export namespace SessionSummary {
}
await Session.updateMessage(userMsg)
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
const assistantMsg = messages.find((m) => m.info.role === "assistant")!
.info as MessageV2.Assistant
const small = await Provider.getSmallModel(assistantMsg.providerID)
if (!small) return
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
const textPart = msgWithParts.parts.find(
(p) => p.type === "text" && !p.synthetic,
) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
maxOutputTokens: small.info.reasoning ? 1500 : 20,
@@ -81,26 +94,33 @@ export namespace SessionSummary {
if (
messages.some(
(m) =>
m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
m.info.role === "assistant" &&
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.
<conversation>
${JSON.stringify(MessageV2.toModelMessage(messages))}
</conversation>
`,
},
],
})
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)
}
}
@@ -114,7 +134,9 @@ export namespace SessionSummary {
let all = await Session.messages(input.sessionID)
if (input.messageID)
all = all.filter(
(x) => x.info.id === input.messageID || (x.info.role === "assistant" && x.info.parentID === input.messageID),
(x) =>
x.info.id === input.messageID ||
(x.info.role === "assistant" && x.info.parentID === input.messageID),
)
return computeDiff({
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.15.20",
"version": "0.15.29",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.15.20",
"version": "0.15.29",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -26,4 +26,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+6 -1
View File
@@ -1,7 +1,12 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from "./types.gen.js"
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client/index.js"
import {
type Config,
type ClientOptions as DefaultClientOptions,
createClient,
createConfig,
} from "./client/index.js"
/**
* The `createClientConfig()` function will be called on client initialization
+3 -1
View File
@@ -107,7 +107,9 @@ export const createClient = (config: Config = {}): Client => {
}
const parseAs =
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"
(opts.parseAs === "auto"
? getParseAs(response.headers.get("Content-Type"))
: opts.parseAs) ?? "json"
let data: any
switch (parseAs) {
+15 -4
View File
@@ -1,7 +1,10 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from "../core/auth.gen.js"
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js"
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from "../core/serverSentEvents.gen.js"
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js"
import type { Middleware } from "./utils.gen.js"
@@ -62,7 +65,11 @@ export interface RequestOptions<
}>,
Pick<
ServerSentEventsOptions<TData>,
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
| "onSseError"
| "onSseEvent"
| "sseDefaultRetryDelay"
| "sseMaxRetryAttempts"
| "sseMaxRetryDelay"
> {
/**
* Any body that you want to add to your request.
@@ -202,7 +209,10 @@ export type Options<
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
"body" | "path" | "query" | "url"
> &
Omit<TData, "url">
export type OptionsLegacyParser<
@@ -211,7 +221,8 @@ export type OptionsLegacyParser<
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> &
TData
: OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers">
+30 -7
View File
@@ -3,11 +3,19 @@
import { getAuthToken } from "../core/auth.gen.js"
import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js"
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "../core/pathSerializer.gen.js"
import { getUrl } from "../core/utils.gen.js"
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = []
if (queryParams && typeof queryParams === "object") {
@@ -77,7 +85,9 @@ export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"
return "formData"
}
if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) {
if (
["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))
) {
return "blob"
}
@@ -97,7 +107,11 @@ const checkForExistence = (
if (!name) {
return false
}
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get("Cookie")?.includes(`${name}=`)
) {
return true
}
return false
@@ -162,7 +176,9 @@ export const mergeConfigs = (a: Config, b: Config): Config => {
return config
}
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => {
export const mergeHeaders = (
...headers: Array<Required<Config>["headers"] | undefined>
): Headers => {
const mergedHeaders = new Headers()
for (const header of headers) {
if (!header || typeof header !== "object") {
@@ -181,7 +197,10 @@ export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | und
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string))
mergedHeaders.set(
key,
typeof value === "object" ? JSON.stringify(value) : (value as string),
)
}
}
}
@@ -197,7 +216,11 @@ type ErrInterceptor<Err, Res, Req, Options> = (
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[]
@@ -31,7 +31,9 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value:
}
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData()
Object.entries(body).forEach(([key, value]) => {
@@ -74,9 +74,9 @@ export const serializeArrayParam = ({
value: unknown[]
}) => {
if (!explode) {
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join(
separatorArrayNoExplode(style),
)
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style))
switch (style) {
case "label":
return `.${joinedValues}`
@@ -106,7 +106,11 @@ export const serializeArrayParam = ({
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues
}
export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => {
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return ""
}
@@ -60,7 +60,11 @@ export interface StreamEvent<TData = unknown> {
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>
}
export const createSseClient = <TData = unknown>({
+9 -2
View File
@@ -1,7 +1,11 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from "./auth.gen.js"
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js"
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from "./bodySerializer.gen.js"
export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
/**
@@ -41,7 +45,10 @@ export interface Config {
*/
headers?:
| RequestInit["headers"]
| Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>
| Record<
string,
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
>
/**
* The request method.
*
+3 -1
View File
@@ -73,7 +73,9 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
continue
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
const replaceValue = encodeURIComponent(
style === "label" ? `.${value as string}` : (value as string),
)
url = url.replace(match, replaceValue)
}
}
+273 -66
View File
@@ -125,6 +125,10 @@ import type {
TuiExecuteCommandErrors,
TuiShowToastData,
TuiShowToastResponses,
TuiControlNextData,
TuiControlNextResponses,
TuiControlResponseData,
TuiControlResponseResponses,
AuthSetData,
AuthSetResponses,
AuthSetErrors,
@@ -133,10 +137,10 @@ import type {
} from "./types.gen.js"
import { client as _heyApiClient } from "./client.gen.js"
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<
TData,
ThrowOnError
> & {
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
> = ClientOptions<TData, ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
@@ -164,7 +168,9 @@ class Project extends _HeyApiClient {
/**
* List all projects
*/
public list<ThrowOnError extends boolean = false>(options?: Options<ProjectListData, ThrowOnError>) {
public list<ThrowOnError extends boolean = false>(
options?: Options<ProjectListData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<ProjectListResponses, unknown, ThrowOnError>({
url: "/project",
...options,
@@ -174,7 +180,9 @@ class Project extends _HeyApiClient {
/**
* Get the current project
*/
public current<ThrowOnError extends boolean = false>(options?: Options<ProjectCurrentData, ThrowOnError>) {
public current<ThrowOnError extends boolean = false>(
options?: Options<ProjectCurrentData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<ProjectCurrentResponses, unknown, ThrowOnError>({
url: "/project/current",
...options,
@@ -196,8 +204,14 @@ class Config extends _HeyApiClient {
/**
* Update config
*/
public update<ThrowOnError extends boolean = false>(options?: Options<ConfigUpdateData, ThrowOnError>) {
return (options?.client ?? this._client).patch<ConfigUpdateResponses, ConfigUpdateErrors, ThrowOnError>({
public update<ThrowOnError extends boolean = false>(
options?: Options<ConfigUpdateData, ThrowOnError>,
) {
return (options?.client ?? this._client).patch<
ConfigUpdateResponses,
ConfigUpdateErrors,
ThrowOnError
>({
url: "/config",
...options,
headers: {
@@ -210,7 +224,9 @@ class Config extends _HeyApiClient {
/**
* List all providers
*/
public providers<ThrowOnError extends boolean = false>(options?: Options<ConfigProvidersData, ThrowOnError>) {
public providers<ThrowOnError extends boolean = false>(
options?: Options<ConfigProvidersData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<ConfigProvidersResponses, unknown, ThrowOnError>({
url: "/config/providers",
...options,
@@ -256,7 +272,9 @@ class Session extends _HeyApiClient {
/**
* List all sessions
*/
public list<ThrowOnError extends boolean = false>(options?: Options<SessionListData, ThrowOnError>) {
public list<ThrowOnError extends boolean = false>(
options?: Options<SessionListData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<SessionListResponses, unknown, ThrowOnError>({
url: "/session",
...options,
@@ -266,8 +284,14 @@ class Session extends _HeyApiClient {
/**
* Create a new session
*/
public create<ThrowOnError extends boolean = false>(options?: Options<SessionCreateData, ThrowOnError>) {
return (options?.client ?? this._client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
public create<ThrowOnError extends boolean = false>(
options?: Options<SessionCreateData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<
SessionCreateResponses,
SessionCreateErrors,
ThrowOnError
>({
url: "/session",
...options,
headers: {
@@ -280,8 +304,14 @@ class Session extends _HeyApiClient {
/**
* Delete a session and all its data
*/
public delete<ThrowOnError extends boolean = false>(options: Options<SessionDeleteData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionDeleteResponses, SessionDeleteErrors, ThrowOnError>({
public delete<ThrowOnError extends boolean = false>(
options: Options<SessionDeleteData, ThrowOnError>,
) {
return (options.client ?? this._client).delete<
SessionDeleteResponses,
SessionDeleteErrors,
ThrowOnError
>({
url: "/session/{id}",
...options,
})
@@ -291,7 +321,11 @@ class Session extends _HeyApiClient {
* Get session
*/
public get<ThrowOnError extends boolean = false>(options: Options<SessionGetData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionGetResponses, SessionGetErrors, ThrowOnError>({
return (options.client ?? this._client).get<
SessionGetResponses,
SessionGetErrors,
ThrowOnError
>({
url: "/session/{id}",
...options,
})
@@ -300,8 +334,14 @@ class Session extends _HeyApiClient {
/**
* Update session properties
*/
public update<ThrowOnError extends boolean = false>(options: Options<SessionUpdateData, ThrowOnError>) {
return (options.client ?? this._client).patch<SessionUpdateResponses, SessionUpdateErrors, ThrowOnError>({
public update<ThrowOnError extends boolean = false>(
options: Options<SessionUpdateData, ThrowOnError>,
) {
return (options.client ?? this._client).patch<
SessionUpdateResponses,
SessionUpdateErrors,
ThrowOnError
>({
url: "/session/{id}",
...options,
headers: {
@@ -314,8 +354,14 @@ class Session extends _HeyApiClient {
/**
* Get a session's children
*/
public children<ThrowOnError extends boolean = false>(options: Options<SessionChildrenData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionChildrenResponses, SessionChildrenErrors, ThrowOnError>({
public children<ThrowOnError extends boolean = false>(
options: Options<SessionChildrenData, ThrowOnError>,
) {
return (options.client ?? this._client).get<
SessionChildrenResponses,
SessionChildrenErrors,
ThrowOnError
>({
url: "/session/{id}/children",
...options,
})
@@ -324,8 +370,14 @@ class Session extends _HeyApiClient {
/**
* Get the todo list for a session
*/
public todo<ThrowOnError extends boolean = false>(options: Options<SessionTodoData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionTodoResponses, SessionTodoErrors, ThrowOnError>({
public todo<ThrowOnError extends boolean = false>(
options: Options<SessionTodoData, ThrowOnError>,
) {
return (options.client ?? this._client).get<
SessionTodoResponses,
SessionTodoErrors,
ThrowOnError
>({
url: "/session/{id}/todo",
...options,
})
@@ -334,8 +386,14 @@ class Session extends _HeyApiClient {
/**
* Analyze the app and create an AGENTS.md file
*/
public init<ThrowOnError extends boolean = false>(options: Options<SessionInitData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionInitResponses, SessionInitErrors, ThrowOnError>({
public init<ThrowOnError extends boolean = false>(
options: Options<SessionInitData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionInitResponses,
SessionInitErrors,
ThrowOnError
>({
url: "/session/{id}/init",
...options,
headers: {
@@ -348,7 +406,9 @@ class Session extends _HeyApiClient {
/**
* Fork an existing session at a specific message
*/
public fork<ThrowOnError extends boolean = false>(options: Options<SessionForkData, ThrowOnError>) {
public fork<ThrowOnError extends boolean = false>(
options: Options<SessionForkData, ThrowOnError>,
) {
return (options.client ?? this._client).post<SessionForkResponses, unknown, ThrowOnError>({
url: "/session/{id}/fork",
...options,
@@ -362,8 +422,14 @@ class Session extends _HeyApiClient {
/**
* Abort a session
*/
public abort<ThrowOnError extends boolean = false>(options: Options<SessionAbortData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionAbortResponses, SessionAbortErrors, ThrowOnError>({
public abort<ThrowOnError extends boolean = false>(
options: Options<SessionAbortData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionAbortResponses,
SessionAbortErrors,
ThrowOnError
>({
url: "/session/{id}/abort",
...options,
})
@@ -372,8 +438,14 @@ class Session extends _HeyApiClient {
/**
* Unshare the session
*/
public unshare<ThrowOnError extends boolean = false>(options: Options<SessionUnshareData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionUnshareResponses, SessionUnshareErrors, ThrowOnError>({
public unshare<ThrowOnError extends boolean = false>(
options: Options<SessionUnshareData, ThrowOnError>,
) {
return (options.client ?? this._client).delete<
SessionUnshareResponses,
SessionUnshareErrors,
ThrowOnError
>({
url: "/session/{id}/share",
...options,
})
@@ -382,8 +454,14 @@ class Session extends _HeyApiClient {
/**
* Share a session
*/
public share<ThrowOnError extends boolean = false>(options: Options<SessionShareData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShareResponses, SessionShareErrors, ThrowOnError>({
public share<ThrowOnError extends boolean = false>(
options: Options<SessionShareData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionShareResponses,
SessionShareErrors,
ThrowOnError
>({
url: "/session/{id}/share",
...options,
})
@@ -392,7 +470,9 @@ class Session extends _HeyApiClient {
/**
* Get the diff that resulted from this user message
*/
public diff<ThrowOnError extends boolean = false>(options: Options<SessionDiffData, ThrowOnError>) {
public diff<ThrowOnError extends boolean = false>(
options: Options<SessionDiffData, ThrowOnError>,
) {
return (options.client ?? this._client).get<SessionDiffResponses, unknown, ThrowOnError>({
url: "/session/{id}/diff",
...options,
@@ -402,8 +482,14 @@ class Session extends _HeyApiClient {
/**
* Summarize the session
*/
public summarize<ThrowOnError extends boolean = false>(options: Options<SessionSummarizeData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionSummarizeResponses, SessionSummarizeErrors, ThrowOnError>({
public summarize<ThrowOnError extends boolean = false>(
options: Options<SessionSummarizeData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionSummarizeResponses,
SessionSummarizeErrors,
ThrowOnError
>({
url: "/session/{id}/summarize",
...options,
headers: {
@@ -416,8 +502,14 @@ class Session extends _HeyApiClient {
/**
* List messages for a session
*/
public messages<ThrowOnError extends boolean = false>(options: Options<SessionMessagesData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessagesResponses, SessionMessagesErrors, ThrowOnError>({
public messages<ThrowOnError extends boolean = false>(
options: Options<SessionMessagesData, ThrowOnError>,
) {
return (options.client ?? this._client).get<
SessionMessagesResponses,
SessionMessagesErrors,
ThrowOnError
>({
url: "/session/{id}/message",
...options,
})
@@ -426,8 +518,14 @@ class Session extends _HeyApiClient {
/**
* Create and send a new message to a session
*/
public prompt<ThrowOnError extends boolean = false>(options: Options<SessionPromptData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionPromptResponses, SessionPromptErrors, ThrowOnError>({
public prompt<ThrowOnError extends boolean = false>(
options: Options<SessionPromptData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionPromptResponses,
SessionPromptErrors,
ThrowOnError
>({
url: "/session/{id}/message",
...options,
headers: {
@@ -440,8 +538,14 @@ class Session extends _HeyApiClient {
/**
* Get a message from a session
*/
public message<ThrowOnError extends boolean = false>(options: Options<SessionMessageData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessageResponses, SessionMessageErrors, ThrowOnError>({
public message<ThrowOnError extends boolean = false>(
options: Options<SessionMessageData, ThrowOnError>,
) {
return (options.client ?? this._client).get<
SessionMessageResponses,
SessionMessageErrors,
ThrowOnError
>({
url: "/session/{id}/message/{messageID}",
...options,
})
@@ -450,8 +554,14 @@ class Session extends _HeyApiClient {
/**
* Send a new command to a session
*/
public command<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionCommandResponses, SessionCommandErrors, ThrowOnError>({
public command<ThrowOnError extends boolean = false>(
options: Options<SessionCommandData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionCommandResponses,
SessionCommandErrors,
ThrowOnError
>({
url: "/session/{id}/command",
...options,
headers: {
@@ -464,8 +574,14 @@ class Session extends _HeyApiClient {
/**
* Run a shell command
*/
public shell<ThrowOnError extends boolean = false>(options: Options<SessionShellData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShellResponses, SessionShellErrors, ThrowOnError>({
public shell<ThrowOnError extends boolean = false>(
options: Options<SessionShellData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionShellResponses,
SessionShellErrors,
ThrowOnError
>({
url: "/session/{id}/shell",
...options,
headers: {
@@ -478,8 +594,14 @@ class Session extends _HeyApiClient {
/**
* Revert a message
*/
public revert<ThrowOnError extends boolean = false>(options: Options<SessionRevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionRevertResponses, SessionRevertErrors, ThrowOnError>({
public revert<ThrowOnError extends boolean = false>(
options: Options<SessionRevertData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionRevertResponses,
SessionRevertErrors,
ThrowOnError
>({
url: "/session/{id}/revert",
...options,
headers: {
@@ -492,8 +614,14 @@ class Session extends _HeyApiClient {
/**
* Restore all reverted messages
*/
public unrevert<ThrowOnError extends boolean = false>(options: Options<SessionUnrevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionUnrevertResponses, SessionUnrevertErrors, ThrowOnError>({
public unrevert<ThrowOnError extends boolean = false>(
options: Options<SessionUnrevertData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
SessionUnrevertResponses,
SessionUnrevertErrors,
ThrowOnError
>({
url: "/session/{id}/unrevert",
...options,
})
@@ -504,7 +632,9 @@ class Command extends _HeyApiClient {
/**
* List all commands
*/
public list<ThrowOnError extends boolean = false>(options?: Options<CommandListData, ThrowOnError>) {
public list<ThrowOnError extends boolean = false>(
options?: Options<CommandListData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<CommandListResponses, unknown, ThrowOnError>({
url: "/command",
...options,
@@ -526,7 +656,9 @@ class Find extends _HeyApiClient {
/**
* Find files
*/
public files<ThrowOnError extends boolean = false>(options: Options<FindFilesData, ThrowOnError>) {
public files<ThrowOnError extends boolean = false>(
options: Options<FindFilesData, ThrowOnError>,
) {
return (options.client ?? this._client).get<FindFilesResponses, unknown, ThrowOnError>({
url: "/find/file",
...options,
@@ -536,7 +668,9 @@ class Find extends _HeyApiClient {
/**
* Find workspace symbols
*/
public symbols<ThrowOnError extends boolean = false>(options: Options<FindSymbolsData, ThrowOnError>) {
public symbols<ThrowOnError extends boolean = false>(
options: Options<FindSymbolsData, ThrowOnError>,
) {
return (options.client ?? this._client).get<FindSymbolsResponses, unknown, ThrowOnError>({
url: "/find/symbol",
...options,
@@ -568,7 +702,9 @@ class File extends _HeyApiClient {
/**
* Get file status
*/
public status<ThrowOnError extends boolean = false>(options?: Options<FileStatusData, ThrowOnError>) {
public status<ThrowOnError extends boolean = false>(
options?: Options<FileStatusData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<FileStatusResponses, unknown, ThrowOnError>({
url: "/file/status",
...options,
@@ -594,7 +730,9 @@ class App extends _HeyApiClient {
/**
* List all agents
*/
public agents<ThrowOnError extends boolean = false>(options?: Options<AppAgentsData, ThrowOnError>) {
public agents<ThrowOnError extends boolean = false>(
options?: Options<AppAgentsData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<AppAgentsResponses, unknown, ThrowOnError>({
url: "/agent",
...options,
@@ -606,7 +744,9 @@ class Mcp extends _HeyApiClient {
/**
* Get MCP server status
*/
public status<ThrowOnError extends boolean = false>(options?: Options<McpStatusData, ThrowOnError>) {
public status<ThrowOnError extends boolean = false>(
options?: Options<McpStatusData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<McpStatusResponses, unknown, ThrowOnError>({
url: "/mcp",
...options,
@@ -614,12 +754,52 @@ class Mcp extends _HeyApiClient {
}
}
class Control extends _HeyApiClient {
/**
* Get the next TUI request from the queue
*/
public next<ThrowOnError extends boolean = false>(
options?: Options<TuiControlNextData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<TuiControlNextResponses, unknown, ThrowOnError>({
url: "/tui/control/next",
...options,
})
}
/**
* Submit a response to the TUI request queue
*/
public response<ThrowOnError extends boolean = false>(
options?: Options<TuiControlResponseData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<
TuiControlResponseResponses,
unknown,
ThrowOnError
>({
url: "/tui/control/response",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
}
class Tui extends _HeyApiClient {
/**
* Append prompt to the TUI
*/
public appendPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiAppendPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiAppendPromptResponses, TuiAppendPromptErrors, ThrowOnError>({
public appendPrompt<ThrowOnError extends boolean = false>(
options?: Options<TuiAppendPromptData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<
TuiAppendPromptResponses,
TuiAppendPromptErrors,
ThrowOnError
>({
url: "/tui/append-prompt",
...options,
headers: {
@@ -632,7 +812,9 @@ class Tui extends _HeyApiClient {
/**
* Open the help dialog
*/
public openHelp<ThrowOnError extends boolean = false>(options?: Options<TuiOpenHelpData, ThrowOnError>) {
public openHelp<ThrowOnError extends boolean = false>(
options?: Options<TuiOpenHelpData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiOpenHelpResponses, unknown, ThrowOnError>({
url: "/tui/open-help",
...options,
@@ -642,7 +824,9 @@ class Tui extends _HeyApiClient {
/**
* Open the session dialog
*/
public openSessions<ThrowOnError extends boolean = false>(options?: Options<TuiOpenSessionsData, ThrowOnError>) {
public openSessions<ThrowOnError extends boolean = false>(
options?: Options<TuiOpenSessionsData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiOpenSessionsResponses, unknown, ThrowOnError>({
url: "/tui/open-sessions",
...options,
@@ -652,7 +836,9 @@ class Tui extends _HeyApiClient {
/**
* Open the theme dialog
*/
public openThemes<ThrowOnError extends boolean = false>(options?: Options<TuiOpenThemesData, ThrowOnError>) {
public openThemes<ThrowOnError extends boolean = false>(
options?: Options<TuiOpenThemesData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiOpenThemesResponses, unknown, ThrowOnError>({
url: "/tui/open-themes",
...options,
@@ -662,7 +848,9 @@ class Tui extends _HeyApiClient {
/**
* Open the model dialog
*/
public openModels<ThrowOnError extends boolean = false>(options?: Options<TuiOpenModelsData, ThrowOnError>) {
public openModels<ThrowOnError extends boolean = false>(
options?: Options<TuiOpenModelsData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiOpenModelsResponses, unknown, ThrowOnError>({
url: "/tui/open-models",
...options,
@@ -672,7 +860,9 @@ class Tui extends _HeyApiClient {
/**
* Submit the prompt
*/
public submitPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiSubmitPromptData, ThrowOnError>) {
public submitPrompt<ThrowOnError extends boolean = false>(
options?: Options<TuiSubmitPromptData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiSubmitPromptResponses, unknown, ThrowOnError>({
url: "/tui/submit-prompt",
...options,
@@ -682,7 +872,9 @@ class Tui extends _HeyApiClient {
/**
* Clear the prompt
*/
public clearPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiClearPromptData, ThrowOnError>) {
public clearPrompt<ThrowOnError extends boolean = false>(
options?: Options<TuiClearPromptData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiClearPromptResponses, unknown, ThrowOnError>({
url: "/tui/clear-prompt",
...options,
@@ -692,8 +884,14 @@ class Tui extends _HeyApiClient {
/**
* Execute a TUI command (e.g. agent_cycle)
*/
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, TuiExecuteCommandErrors, ThrowOnError>({
public executeCommand<ThrowOnError extends boolean = false>(
options?: Options<TuiExecuteCommandData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<
TuiExecuteCommandResponses,
TuiExecuteCommandErrors,
ThrowOnError
>({
url: "/tui/execute-command",
...options,
headers: {
@@ -706,7 +904,9 @@ class Tui extends _HeyApiClient {
/**
* Show a toast notification in the TUI
*/
public showToast<ThrowOnError extends boolean = false>(options?: Options<TuiShowToastData, ThrowOnError>) {
public showToast<ThrowOnError extends boolean = false>(
options?: Options<TuiShowToastData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<TuiShowToastResponses, unknown, ThrowOnError>({
url: "/tui/show-toast",
...options,
@@ -716,6 +916,7 @@ class Tui extends _HeyApiClient {
},
})
}
control = new Control({ client: this._client })
}
class Auth extends _HeyApiClient {
@@ -738,8 +939,14 @@ class Event extends _HeyApiClient {
/**
* Get events
*/
public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
return (options?.client ?? this._client).get.sse<EventSubscribeResponses, unknown, ThrowOnError>({
public subscribe<ThrowOnError extends boolean = false>(
options?: Options<EventSubscribeData, ThrowOnError>,
) {
return (options?.client ?? this._client).get.sse<
EventSubscribeResponses,
unknown,
ThrowOnError
>({
url: "/event",
...options,
})
+64 -2
View File
@@ -275,6 +275,10 @@ export type McpLocalConfig = {
* Enable or disable the MCP server on startup
*/
enabled?: boolean
/**
* Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
*/
timeout?: number
}
export type McpRemoteConfig = {
@@ -296,6 +300,10 @@ export type McpRemoteConfig = {
headers?: {
[key: string]: string
}
/**
* Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.
*/
timeout?: number
}
/**
@@ -505,6 +513,10 @@ export type Config = {
}
}>
}
/**
* Number of retries for chat completions on failure
*/
chatMaxRetries?: number
disable_paste_summary?: boolean
}
}
@@ -658,7 +670,12 @@ export type AssistantMessage = {
created: number
completed?: number
}
error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
error?:
| ProviderAuthError
| UnknownError
| MessageOutputLengthError
| MessageAbortedError
| ApiError
system: Array<string>
parentID: string
modelID: string
@@ -1222,7 +1239,12 @@ export type EventSessionError = {
type: "session.error"
properties: {
sessionID?: string
error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
error?:
| ProviderAuthError
| UnknownError
| MessageOutputLengthError
| MessageAbortedError
| ApiError
}
}
@@ -2618,6 +2640,46 @@ export type TuiShowToastResponses = {
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
export type TuiControlNextData = {
body?: never
path?: never
query?: {
directory?: string
}
url: "/tui/control/next"
}
export type TuiControlNextResponses = {
/**
* Next TUI request
*/
200: {
path: string
body: unknown
}
}
export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses]
export type TuiControlResponseData = {
body?: unknown
path?: never
query?: {
directory?: string
}
url: "/tui/control/response"
}
export type TuiControlResponseResponses = {
/**
* Response submitted successfully
*/
200: boolean
}
export type TuiControlResponseResponse =
TuiControlResponseResponses[keyof TuiControlResponseResponses]
export type AuthSetData = {
body?: Auth
path: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "0.15.20",
"version": "0.15.29",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+7
View File
@@ -13,9 +13,11 @@ import (
flag "github.com/spf13/pflag"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode-sdk-go/packages/ssestream"
"github.com/sst/opencode/internal/api"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/clipboard"
"github.com/sst/opencode/internal/decoders"
"github.com/sst/opencode/internal/tui"
"github.com/sst/opencode/internal/util"
"golang.org/x/sync/errgroup"
@@ -61,6 +63,11 @@ func main() {
}
}
// Register custom SSE decoder to handle large events (>32MB)
// This is a workaround for the bufio.Scanner token size limit in the auto-generated SDK
// See: packages/tui/internal/decoders/decoder.go
ssestream.RegisterDecoder("text/event-stream", decoders.NewUnboundedDecoder)
httpClient := opencode.NewClient(
option.WithBaseURL(url),
)
@@ -504,7 +504,11 @@ func renderToolDetails(
base := styles.NewStyle().Background(backgroundColor)
text := base.Foreground(t.Text()).Bold(true).Render
muted := base.Foreground(t.TextMuted()).Render
permissionContent = "Permission required to run this tool:\n\n"
if permission.Type == "doom-loop" {
permissionContent = permission.Title + "\n\n"
} else {
permissionContent = "Permission required to run this tool:\n\n"
}
permissionContent += text(
"enter ",
) + muted(
@@ -642,9 +646,9 @@ func renderToolDetails(
for _, item := range todos.([]any) {
todo := item.(map[string]any)
content := todo["content"]
if content == nil {
continue
}
if content == nil {
continue
}
switch todo["status"] {
case "completed":
body += fmt.Sprintf("- [x] %s\n", content)
@@ -656,6 +656,8 @@ func (m *messagesComponent) renderView() tea.Cmd {
case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded"
case opencode.AssistantMessageErrorAPIError:
error = err.Data.Message
case opencode.ProviderAuthError:
error = err.Data.Message
case opencode.MessageAbortedError:
+118
View File
@@ -0,0 +1,118 @@
package decoders
import (
"bufio"
"bytes"
"io"
"github.com/sst/opencode-sdk-go/packages/ssestream"
)
// UnboundedDecoder is an SSE decoder that uses bufio.Reader instead of bufio.Scanner
// to avoid the 32MB token size limit. This is a workaround for large SSE events until
// the upstream Stainless SDK is fixed.
//
// This decoder handles SSE events of unlimited size by reading line-by-line with
// bufio.Reader.ReadBytes('\n'), which dynamically grows the buffer as needed.
type UnboundedDecoder struct {
reader *bufio.Reader
closer io.ReadCloser
evt ssestream.Event
err error
}
// NewUnboundedDecoder creates a new unbounded SSE decoder with a 1MB initial buffer size
func NewUnboundedDecoder(rc io.ReadCloser) ssestream.Decoder {
reader := bufio.NewReaderSize(rc, 1024*1024) // 1MB initial buffer
return &UnboundedDecoder{
reader: reader,
closer: rc,
}
}
// Next reads and decodes the next SSE event from the stream
func (d *UnboundedDecoder) Next() bool {
if d.err != nil {
return false
}
event := ""
data := bytes.NewBuffer(nil)
for {
line, err := d.reader.ReadBytes('\n')
if err != nil {
if err == io.EOF && len(line) == 0 {
return false
}
if err != io.EOF {
d.err = err
return false
}
}
// Remove trailing newline characters
line = bytes.TrimRight(line, "\r\n")
// Empty line indicates end of event
if len(line) == 0 {
if data.Len() > 0 || event != "" {
d.evt = ssestream.Event{
Type: event,
Data: data.Bytes(),
}
return true
}
continue
}
// Skip comments (lines starting with ':')
if line[0] == ':' {
continue
}
// Parse field
name, value, found := bytes.Cut(line, []byte(":"))
if !found {
// Field with no value
continue
}
// Remove leading space from value
if len(value) > 0 && value[0] == ' ' {
value = value[1:]
}
switch string(name) {
case "":
// An empty line in the form ": something" is a comment and should be ignored
continue
case "event":
event = string(value)
case "data":
_, d.err = data.Write(value)
if d.err != nil {
return false
}
_, d.err = data.WriteRune('\n')
if d.err != nil {
return false
}
}
}
}
// Event returns the current event
func (d *UnboundedDecoder) Event() ssestream.Event {
return d.evt
}
// Close closes the underlying reader
func (d *UnboundedDecoder) Close() error {
return d.closer.Close()
}
// Err returns any error that occurred during decoding
func (d *UnboundedDecoder) Err() error {
return d.err
}
@@ -0,0 +1,194 @@
package decoders
import (
"bytes"
"io"
"strings"
"testing"
)
func TestUnboundedDecoder_SmallEvent(t *testing.T) {
data := "event: test\ndata: hello world\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "test" {
t.Errorf("Expected event type 'test', got '%s'", evt.Type)
}
if string(evt.Data) != "hello world\n" {
t.Errorf("Expected data 'hello world\\n', got '%s'", string(evt.Data))
}
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_LargeEvent(t *testing.T) {
// Create a large event (50MB)
size := 50 * 1024 * 1024
largeData := strings.Repeat("x", size)
var buf bytes.Buffer
buf.WriteString("event: large\n")
buf.WriteString("data: ")
buf.WriteString(largeData)
buf.WriteString("\n\n")
rc := io.NopCloser(&buf)
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "large" {
t.Errorf("Expected event type 'large', got '%s'", evt.Type)
}
expectedData := largeData + "\n"
if string(evt.Data) != expectedData {
t.Errorf("Data size mismatch: expected %d, got %d", len(expectedData), len(evt.Data))
}
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_MultipleEvents(t *testing.T) {
data := "event: first\ndata: data1\n\nevent: second\ndata: data2\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
// First event
if !decoder.Next() {
t.Fatal("Expected Next() to return true for first event")
}
evt := decoder.Event()
if evt.Type != "first" {
t.Errorf("Expected event type 'first', got '%s'", evt.Type)
}
if string(evt.Data) != "data1\n" {
t.Errorf("Expected data 'data1\\n', got '%s'", string(evt.Data))
}
// Second event
if !decoder.Next() {
t.Fatal("Expected Next() to return true for second event")
}
evt = decoder.Event()
if evt.Type != "second" {
t.Errorf("Expected event type 'second', got '%s'", evt.Type)
}
if string(evt.Data) != "data2\n" {
t.Errorf("Expected data 'data2\\n', got '%s'", string(evt.Data))
}
// No more events
if decoder.Next() {
t.Error("Expected Next() to return false at end of stream")
}
if err := decoder.Err(); err != nil {
t.Errorf("Expected no error, got %v", err)
}
}
func TestUnboundedDecoder_MultilineData(t *testing.T) {
data := "event: multiline\ndata: line1\ndata: line2\ndata: line3\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "multiline" {
t.Errorf("Expected event type 'multiline', got '%s'", evt.Type)
}
expectedData := "line1\nline2\nline3\n"
if string(evt.Data) != expectedData {
t.Errorf("Expected data '%s', got '%s'", expectedData, string(evt.Data))
}
}
func TestUnboundedDecoder_Comments(t *testing.T) {
data := ": this is a comment\nevent: test\n: another comment\ndata: hello\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "test" {
t.Errorf("Expected event type 'test', got '%s'", evt.Type)
}
if string(evt.Data) != "hello\n" {
t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
}
}
func TestUnboundedDecoder_NoEventType(t *testing.T) {
data := "data: hello\n\n"
rc := io.NopCloser(strings.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
t.Fatal("Expected Next() to return true")
}
evt := decoder.Event()
if evt.Type != "" {
t.Errorf("Expected empty event type, got '%s'", evt.Type)
}
if string(evt.Data) != "hello\n" {
t.Errorf("Expected data 'hello\\n', got '%s'", string(evt.Data))
}
}
func BenchmarkUnboundedDecoder_LargeEvent(b *testing.B) {
// Create a 10MB event for benchmarking
size := 10 * 1024 * 1024
largeData := strings.Repeat("x", size)
var buf bytes.Buffer
buf.WriteString("event: bench\n")
buf.WriteString("data: ")
buf.WriteString(largeData)
buf.WriteString("\n\n")
data := buf.Bytes()
b.ResetTimer()
b.SetBytes(int64(len(data)))
for i := 0; i < b.N; i++ {
rc := io.NopCloser(bytes.NewReader(data))
decoder := NewUnboundedDecoder(rc)
if !decoder.Next() {
b.Fatal("Expected Next() to return true")
}
_ = decoder.Event()
}
}
+14
View File
@@ -656,12 +656,26 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionError:
switch err := msg.Properties.Error.AsUnion().(type) {
case nil:
// No error details provided
case opencode.ProviderAuthError:
slog.Error("Failed to authenticate with provider", "error", err.Data.Message)
return a, toast.NewErrorToast("Provider error: " + err.Data.Message)
case opencode.UnknownError:
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
case opencode.EventListResponseEventSessionErrorPropertiesErrorAPIError:
slog.Error("API error", "message", err.Data.Message, "statusCode", err.Data.StatusCode)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
case opencode.MessageAbortedError:
// Message was aborted - this is expected when user cancels, so just log it
slog.Debug("Message aborted", "message", err.Data.Message)
case opencode.EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError:
slog.Error("Message output length error")
return a, toast.NewErrorToast("Message output length exceeded limit")
default:
// Handle any unhandled error types
slog.Error("Unhandled session error type", "type", fmt.Sprintf("%T", err))
return a, toast.NewErrorToast("An unexpected error occurred")
}
case opencode.EventListResponseEventSessionCompacted:
if msg.Properties.SessionID == a.app.Session.ID {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.15.20",
"version": "0.15.29",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+4 -2
View File
@@ -2,7 +2,7 @@
[data-slot="diff-hunk-separator-line-number"] {
position: sticky;
left: 0;
background-color: hsla(209, 96%, 90%, 1);
background-color: var(--surface-diff-hidden-strong);
z-index: 2;
display: flex;
align-items: center;
@@ -12,11 +12,13 @@
aspect-ratio: 1;
width: 24px;
height: 24px;
color: var(--icon-strong-base);
}
}
[data-slot="diff-hunk-separator-content"] {
position: sticky;
background-color: hsla(210, 100%, 96%, 1);
background-color: var(--surface-diff-hidden-base);
color: var(--text-base);
width: var(--pjs-column-content-width);
left: var(--pjs-column-number-width);
padding-left: 8px;
+5 -3
View File
@@ -54,18 +54,20 @@ export function Diff<T>(props: DiffProps<T>) {
// When ready to render, simply call .render with old/new file, optional
// annotations and a container element to hold the diff
createEffect(() => {
// @ts-expect-error
const instance = new FileDiff<T>({
theme: "pierre-light",
// theme: "pierre-light",
// theme: "pierre-light",
// Or can also provide a 'themes' prop, which allows the code to adapt
// to your OS light or dark theme
// themes: { dark: 'pierre-night', light: 'pierre-light' },
themes: { dark: "pierre-dark", light: "pierre-light" },
// When using the 'themes' prop, 'themeType' allows you to force 'dark'
// or 'light' theme, or inherit from the OS ('system') theme.
themeType: "system",
// Disable the line numbers for your diffs, generally not recommended
disableLineNumbers: false,
// Whether code should 'wrap' with long lines or 'scroll'.
overflow: "scroll",
overflow: "wrap",
// Normally you shouldn't need this prop, but if you don't provide a
// valid filename or your file doesn't have an extension you may want to
// override the automatic detection. You can specify that language here:
+50 -2
View File
@@ -34,8 +34,8 @@
--smoke-dark-alpha-9: #faf5f467;
--smoke-dark-alpha-10: #fbf5f576;
--smoke-dark-alpha-11: #fcf9f9b2;
--smoke-light-alpha-1: #55000003;
--smoke-dark-alpha-12: #fdfbfbf0;
--smoke-light-alpha-1: #55000003;
--smoke-light-alpha-2: #25000007;
--smoke-light-alpha-3: #1100000f;
--smoke-light-alpha-4: #0c000017;
@@ -125,8 +125,8 @@
--cobalt-dark-alpha-9: #034cff;
--cobalt-dark-alpha-10: #003bffed;
--cobalt-dark-alpha-11: #89b5ff;
--cobalt-light-8: #73a4ff;
--cobalt-dark-alpha-12: #cde2ff;
--cobalt-light-8: #73a4ff;
--cobalt-light-9: #034cff;
--cobalt-light-10: #0443de;
--cobalt-light-11: #1251ec;
@@ -445,4 +445,52 @@
--mint-light-alpha-10: #0cc7006c;
--mint-light-alpha-11: #016800cf;
--mint-light-alpha-12: #022e00e2;
--blue-dark-1: #0e161f;
--blue-dark-2: #0f1b2d;
--blue-dark-3: #0f233c;
--blue-dark-4: #10294b;
--blue-dark-5: #0e2f57;
--blue-dark-6: #0c3768;
--blue-dark-7: #094280;
--blue-dark-8: #0854a4;
--blue-dark-9: #0091ff;
--blue-dark-10: #389eff;
--blue-dark-11: #51a8ff;
--blue-dark-12: #eaf6ff;
--blue-light-1: #f9fcff;
--blue-light-2: #f5faff;
--blue-light-3: #eaf4ff;
--blue-light-4: #e0efff;
--blue-light-5: #cde6fd;
--blue-light-6: #b9d9f8;
--blue-light-7: #96c7f2;
--blue-light-8: #5cafee;
--blue-light-9: #0091ff;
--blue-light-10: #007fef;
--blue-light-11: #0069db;
--blue-light-12: #00254d;
--blue-dark-alpha-1: #00000000;
--blue-dark-alpha-2: #0c58fc0f;
--blue-dark-alpha-3: #1576fd23;
--blue-dark-alpha-4: #1576fd33;
--blue-dark-alpha-5: #107bfd3f;
--blue-dark-alpha-6: #0a7cff51;
--blue-dark-alpha-7: #057dff70;
--blue-dark-alpha-8: #057dff99;
--blue-dark-alpha-9: #0094fff9;
--blue-dark-alpha-10: #38a2fff9;
--blue-dark-alpha-11: #51abfff9;
--blue-dark-alpha-12: #effbfff9;
--blue-light-alpha-1: #0582ff05;
--blue-light-alpha-2: #0582ff0a;
--blue-light-alpha-3: #007fff11;
--blue-light-alpha-4: #007fff1e;
--blue-light-alpha-5: #017fee30;
--blue-light-alpha-6: #0176e447;
--blue-light-alpha-7: #0077e068;
--blue-light-alpha-8: #0082e5a0;
--blue-light-alpha-9: #0090fff9;
--blue-light-alpha-10: #007feff9;
--blue-light-alpha-11: #0066dbf9;
--blue-light-alpha-12: #002047f9;
}
+20
View File
@@ -11,6 +11,26 @@
--spacing: 0.25rem;
/* --spacing: var(--spacing); */
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
--container-3xs: 16rem;
--container-2xs: 18rem;
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
--container-xl: 36rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem;
--container-7xl: 80rem;
--font-sans: var(--font-family-sans);
--font-sans--font-feature-settings: var(--font-family-sans--font-feature-settings);
--font-mono: var(--font-family-mono);
+36 -30
View File
@@ -50,9 +50,11 @@
--radius-4xl: 2rem;
--shadow-xs:
0 1px 2px -1px rgba(19, 16, 16, 0.04), 0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08);
0 1px 2px -1px rgba(19, 16, 16, 0.04), 0 1px 2px 0 rgba(19, 16, 16, 0.06),
0 1px 3px 0 rgba(19, 16, 16, 0.08);
--shadow-md:
0 6px 8px -4px rgba(19, 16, 16, 0.12), 0 4px 3px -2px rgba(19, 16, 16, 0.12), 0 1px 2px -1px rgba(19, 16, 16, 0.12);
0 6px 8px -4px rgba(19, 16, 16, 0.12), 0 4px 3px -2px rgba(19, 16, 16, 0.12),
0 1px 2px -1px rgba(19, 16, 16, 0.12);
--shadow-xs-border-selected:
0 0 0 3px var(--border-weak-selected, rgba(1, 103, 255, 0.29)),
0 0 0 1px var(--border-selected, rgba(0, 74, 255, 0.99)), 0 1px 2px -1px rgba(19, 16, 16, 0.25),
@@ -66,8 +68,8 @@
--background-weak: var(--smoke-light-3);
--background-strong: var(--smoke-light-1);
--background-stronger: #fcfcfc;
--base: var(--smoke-light-alpha-2);
--surface-base: var(--smoke-light-alpha-2);
--base: var(--smoke-light-alpha-2);
--surface-base-hover: #0500000f;
--surface-base-active: var(--smoke-light-alpha-3);
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
@@ -81,6 +83,7 @@
--surface-float-base: var(--smoke-dark-1);
--surface-float-base-hover: var(--smoke-dark-2);
--surface-raised-base-hover: var(--smoke-light-alpha-2);
--surface-raised-base-active: var(--smoke-light-alpha-3);
--surface-raised-strong: var(--smoke-light-1);
--surface-raised-strong-hover: var(--white);
--surface-raised-stronger: var(--white);
@@ -107,9 +110,9 @@
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-skip-base: var(--smoke-light-2);
--surface-diff-unchanged-base: #ffffff00;
--surface-diff-skip-base: var(--smoke-light-2);
--surface-diff-hidden-base: var(--blue-light-3);
--surface-diff-hidden-weak: var(--blue-light-2);
--surface-diff-hidden-weaker: var(--blue-light-1);
--surface-diff-hidden-strong: var(--blue-light-5);
@@ -142,7 +145,7 @@
--text-on-warning-base: var(--smoke-dark-alpha-11);
--text-on-info-base: var(--smoke-dark-alpha-11);
--text-diff-add-base: var(--mint-light-11);
--text-diff-delete-base: var(--ember-light-11);
--text-diff-delete-base: var(--ember-light-10);
--text-diff-delete-strong: var(--ember-light-12);
--text-diff-add-strong: var(--mint-light-12);
--text-on-info-weak: var(--smoke-dark-alpha-9);
@@ -198,19 +201,19 @@
--icon-selected: var(--smoke-light-12);
--icon-disabled: var(--smoke-light-8);
--icon-focus: var(--smoke-light-12);
--icon-weak-base: var(--smoke-light-7);
--icon-invert-base: #ffffff;
--icon-weak-base: var(--smoke-light-7);
--icon-weak-hover: var(--smoke-light-8);
--icon-weak-active: var(--smoke-light-9);
--icon-weak-selected: var(--smoke-light-10);
--icon-weak-disabled: var(--smoke-light-6);
--icon-weak-focus: var(--smoke-light-9);
--icon-strong-base: var(--smoke-light-12);
--icon-strong-hover: var(--smoke-light-12);
--icon-strong-active: var(--smoke-light-12);
--icon-strong-selected: var(--smoke-light-12);
--icon-strong-hover: #151313;
--icon-strong-active: #020202;
--icon-strong-selected: #020202;
--icon-strong-disabled: var(--smoke-light-8);
--icon-strong-focus: var(--smoke-light-12);
--icon-strong-focus: #020202;
--icon-brand-base: var(--smoke-light-12);
--icon-interactive-base: var(--cobalt-light-9);
--icon-success-base: var(--apple-light-7);
@@ -248,9 +251,8 @@
--icon-diff-add-base: var(--mint-light-11);
--icon-diff-add-hover: var(--mint-light-12);
--icon-diff-add-active: var(--mint-light-12);
--icon-diff-delete-base: var(--ember-light-9);
--icon-diff-delete-hover: var(--ember-light-10);
--icon-diff-delete-active: var(--ember-light-11);
--icon-diff-delete-base: var(--ember-light-10);
--icon-diff-delete-hover: var(--ember-light-11);
--syntax-comment: #8a8a8a;
--syntax-string: #d68c27;
--syntax-keyword: #3b7dd8;
@@ -286,6 +288,8 @@
--border-weaker-selected: var(--cobalt-light-alpha-4);
--border-weaker-disabled: var(--smoke-light-alpha-2);
--border-weaker-focus: var(--smoke-light-alpha-6);
--button-ghost-hover: var(--smoke-light-alpha-2);
--button-ghost-hover2: var(--smoke-light-alpha-3);
@media (prefers-color-scheme: dark) {
/* OC-1-Dark */
@@ -294,8 +298,8 @@
--background-weak: #201d1d;
--background-strong: #151313;
--background-stronger: #201c1c;
--base: var(--smoke-dark-alpha-2);
--surface-base: var(--smoke-dark-alpha-2);
--base: var(--smoke-dark-alpha-2);
--surface-base-hover: #e0b7b716;
--surface-base-active: var(--smoke-dark-alpha-3);
--surface-base-interactive-active: var(--cobalt-dark-alpha-2);
@@ -309,6 +313,7 @@
--surface-float-base: var(--smoke-dark-1);
--surface-float-base-hover: var(--smoke-dark-2);
--surface-raised-base-hover: var(--smoke-dark-alpha-4);
--surface-raised-base-active: var(--smoke-dark-alpha-5);
--surface-raised-strong: var(--smoke-dark-alpha-4);
--surface-raised-strong-hover: var(--smoke-dark-alpha-6);
--surface-raised-stronger: var(--smoke-dark-alpha-6);
@@ -329,15 +334,15 @@
--surface-warning-base: var(--solaris-light-3);
--surface-warning-weak: var(--solaris-light-2);
--surface-warning-strong: var(--solaris-light-9);
--surface-critical-base: var(--ember-light-3);
--surface-critical-weak: var(--ember-light-2);
--surface-critical-strong: var(--ember-light-9);
--surface-critical-base: var(--ember-dark-3);
--surface-critical-weak: var(--ember-dark-2);
--surface-critical-strong: var(--ember-dark-9);
--surface-info-base: var(--lilac-light-3);
--surface-info-weak: var(--lilac-light-2);
--surface-info-strong: var(--lilac-light-9);
--surface-diff-hidden-base: var(--blue-dark-2);
--surface-diff-skip-base: var(--smoke-dark-alpha-1);
--surface-diff-unchanged-base: var(--smoke-dark-1);
--surface-diff-skip-base: var(--smoke-dark-alpha-1);
--surface-diff-hidden-base: var(--blue-dark-2);
--surface-diff-hidden-weak: var(--blue-dark-1);
--surface-diff-hidden-weaker: var(--blue-dark-3);
--surface-diff-hidden-strong: var(--blue-dark-5);
@@ -414,9 +419,9 @@
--border-warning-base: var(--solaris-light-6);
--border-warning-hover: var(--solaris-light-7);
--border-warning-selected: var(--solaris-light-9);
--border-critical-base: var(--ember-light-6);
--border-critical-hover: var(--ember-light-7);
--border-critical-selected: var(--ember-light-9);
--border-critical-base: var(--ember-dark-5);
--border-critical-hover: var(--ember-dark-7);
--border-critical-selected: var(--ember-dark-9);
--border-info-base: var(--lilac-light-6);
--border-info-hover: var(--lilac-light-7);
--border-info-selected: var(--lilac-light-9);
@@ -426,19 +431,19 @@
--icon-selected: var(--smoke-dark-12);
--icon-disabled: var(--smoke-dark-7);
--icon-focus: var(--smoke-dark-12);
--icon-weak-base: var(--smoke-dark-6);
--icon-invert-base: var(--smoke-dark-1);
--icon-weak-base: var(--smoke-dark-6);
--icon-weak-hover: var(--smoke-light-7);
--icon-weak-active: var(--smoke-light-8);
--icon-weak-selected: var(--smoke-light-9);
--icon-weak-disabled: var(--smoke-light-4);
--icon-weak-focus: var(--smoke-light-9);
--icon-strong-base: var(--smoke-dark-12);
--icon-strong-hover: var(--smoke-light-12);
--icon-strong-active: var(--smoke-light-12);
--icon-strong-selected: var(--smoke-light-12);
--icon-strong-disabled: var(--smoke-light-8);
--icon-strong-focus: var(--smoke-light-12);
--icon-strong-hover: #f6f3f3;
--icon-strong-active: #fcfcfc;
--icon-strong-selected: #fdfcfc;
--icon-strong-disabled: var(--smoke-dark-8);
--icon-strong-focus: #fdfcfc;
--icon-brand-base: var(--white);
--icon-interactive-base: var(--cobalt-dark-9);
--icon-success-base: var(--apple-dark-7);
@@ -478,7 +483,6 @@
--icon-diff-add-active: var(--mint-dark-11);
--icon-diff-delete-base: var(--ember-dark-9);
--icon-diff-delete-hover: var(--ember-dark-10);
--icon-diff-delete-active: var(--ember-dark-11);
--syntax-comment: #808080;
--syntax-string: #9d7cd8;
--syntax-keyword: #fab283;
@@ -514,5 +518,7 @@
--border-weaker-selected: var(--cobalt-dark-alpha-3);
--border-weaker-disabled: var(--smoke-dark-alpha-2);
--border-weaker-focus: var(--smoke-dark-alpha-6);
--button-ghost-hover: var(--smoke-dark-alpha-2);
--button-ghost-hover2: var(--smoke-dark-alpha-3);
}
}
+55 -19
View File
@@ -49,31 +49,67 @@
}
.scroller {
--fade-height: 1.5rem;
/* --fade-height: 1.5rem; */
/**/
/* --mask-top: linear-gradient(to bottom, transparent, black var(--fade-height)); */
/* --mask-bottom: linear-gradient(to top, transparent, black var(--fade-height)); */
/**/
/* mask-image: var(--mask-top), var(--mask-bottom); */
/* mask-repeat: no-repeat; */
/* mask-size: 100% var(--fade-height); */
--mask-top: linear-gradient(to bottom, transparent, black var(--fade-height));
--mask-bottom: linear-gradient(to top, transparent, black var(--fade-height));
mask-image: var(--mask-top), var(--mask-bottom);
mask-repeat: no-repeat;
mask-size: 100% var(--fade-height);
animation: adjust-masks linear;
animation: scroll-fade linear;
animation-timeline: scroll(self);
}
@keyframes adjust-masks {
from {
mask-position:
0 calc(0% - var(--fade-height)),
0 100%;
/* Define the keyframes for the mask.
These percentages now map to scroll positions:
0% = Scrolled to the top
100% = Scrolled to the bottom
*/
@keyframes scroll-fade {
/* At the very top (0% scroll) */
0% {
mask-image: linear-gradient(
to bottom,
black 90%,
/* Opaque, but start fade to bottom */ transparent 100%
);
}
to {
mask-position:
0 0,
0 calc(100% + var(--fade-height));
/* A small amount scrolled (e.g., 5%)
This is where the top fade should be fully visible.
*/
5% {
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10%,
/* Fade-in top */ black 90%,
/* Fade-out bottom */ transparent 100%
);
}
/* Nearing the bottom (e.g., 95%)
The bottom fade should start disappearing.
*/
95% {
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10%,
/* Fade-in top */ black 90%,
/* Fade-out bottom */ transparent 100%
);
}
/* At the very bottom (100% scroll) */
100% {
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10% /* Opaque, but start fade from top */
);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "0.15.20",
"version": "0.15.29",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
@@ -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
+89 -24
View File
@@ -1,15 +1,18 @@
---
title: Enterprise
description: Using OpenCode in your organization.
description: Using OpenCode securely in your organization.
---
import config from "../../../config.mjs"
export const email = `mailto:${config.email}`
OpenCode does not store any of your code or context data. This makes it easy for
you to use OpenCode at your organization.
OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
To get started, we recommend:
:::note
OpenCode does not store any of your code or context data.
:::
To get started with OpenCode Enterprise:
1. Do a trial internally with your team.
2. **<a href={email}>Contact us</a>** to discuss pricing and implementation options.
@@ -18,13 +21,16 @@ To get started, we recommend:
## Trial
Since OpenCode is open source and does not store any of your code or context data, your developers can simply [get started](/docs/) and carry out a trial.
OpenCode is open source and does not store any of your code or context data, so your developers can simply [get started](/docs/) and carry out a trial.
---
### Data handling
**opencode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
**OpenCode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
This means that as long as you are using a provider you trust, or an internal
AI gateway, you can use OpenCode securely.
The only caveat here is the optional `/share` feature.
@@ -32,7 +38,7 @@ The only caveat here is the optional `/share` feature.
#### Sharing conversations
If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these shares pages at opencode.ai.
If a user enables the `/share` feature, the conversation and the data associated with it are sent to the service we use to host these share pages at opencode.ai.
The data is currently served through our CDN's edge network, and is cached on the edge near your users.
@@ -51,29 +57,93 @@ We recommend you disable this for your trial.
### Code ownership
**You own all code produced by opencode.** There are no licensing restrictions or ownership claims.
**You own all code produced by OpenCode.** There are no licensing restrictions or ownership claims.
---
## Pricing
We use a per-seat model for OpenCode Enterprise. If you have your own LLM gateway, we do not charge for tokens used. For further details about pricing and implementation options, **<a href={email}>contact us</a>**.
---
## Deployment
Once you have completed your trial and you are ready to self-host opencode at
Once you have completed your trial and you are ready to use OpenCode at
your organization, you can **<a href={email}>contact us</a>** to discuss
pricing and implementation options.
---
### SSO
### Central Config
SSO integration can be implemented for enterprise deployments after your trial.
This will allow your team's session data and shared conversations to be protected
by your enterprise's authentication system.
We can set up OpenCode to use a single central config for your entire organization.
This centralized config can integrate with your SSO provider and ensures all users access only your internal AI gateway.
---
### Private NPM
### SSO integration
opencode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running opencode.
Through the central config, OpenCode can integrate with your organization's SSO provider for authentication.
This allows OpenCode to obtain credentials for your internal AI gateway through your existing identity management system.
---
### Internal AI gateway
With the central config, OpenCode can also be configured to use only your internal AI gateway.
You can also disable all other AI providers, ensuring all requests go through your organization's approved infrastructure.
---
### Self-hosting
While we recommend disabling the share pages to ensure your data never leaves
your organization, we can also help you self-host them on your infrastructure.
This is currently on our roadmap. If you're interested, **<a href={email}>let us know</a>**.
---
## FAQ
<details>
<summary>What is OpenCode Enterprise?</summary>
OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.
</details>
<details>
<summary>How do I get started with OpenCode Enterprise?</summary>
Simply start with an internal trial with your team. OpenCode by default does not store your code or context data, making it easy to get started.
Then **<a href={email}>contact us</a>** to discuss pricing and implementation options.
</details>
<details>
<summary>How does enterprise pricing work?</summary>
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens used. For further details, **<a href={email}>contact us</a>** for a custom quote based on your organization's needs.
</details>
<details>
<summary>Is my data secure with OpenCode Enterprise?</summary>
Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.
</details>
<details>
<summary>Can we use our own private NPM registry?</summary>
OpenCode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running OpenCode.
To set up authentication with your private registry:
@@ -81,11 +151,11 @@ To set up authentication with your private registry:
npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/
```
This creates `~/.npmrc` with authentication details. opencode will automatically
This creates `~/.npmrc` with authentication details. OpenCode will automatically
pick this up.
:::caution
You must be logged into the private registry before running opencode.
You must be logged into the private registry before running OpenCode.
:::
Alternatively, you can manually configure a `.npmrc` file:
@@ -95,11 +165,6 @@ registry=https://your-company.jfrog.io/api/npm/npm-virtual/
//your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN}
```
Developers must be logged into the private registry before running opencode to ensure packages can be installed from your enterprise registry.
Developers must be logged into the private registry before running OpenCode to ensure packages can be installed from your enterprise registry.
---
### Self-hosting
The share feature can be self-hosted and the share pages can be made accessible
only after the user has been authenticated.
</details>
+23 -21
View File
@@ -45,12 +45,12 @@ with a unique name. You can refer to that MCP by name when prompting the LLM.
"mcp": {
"name-of-mcp-server": {
// ...
"enabled": true
"enabled": true,
},
"name-of-other-mcp-server": {
// ...
}
}
},
},
}
```
@@ -72,10 +72,10 @@ Add local MCP servers using `type` to `"local"` within the MCP object.
"command": ["npx", "-y", "my-mcp-command"],
"enabled": true,
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
}
}
"MY_ENV_VAR": "my_env_var_value",
},
},
},
}
```
@@ -91,8 +91,8 @@ For example, here's how I can add the test
"mcp_everything": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-everything"],
}
}
},
},
}
```
@@ -106,12 +106,13 @@ use the mcp_everything tool to add the number 3 and 4
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. |
| 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. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---
@@ -139,12 +140,13 @@ Here the `url` is the URL of the remote MCP server and with the `headers` option
#### 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. |
| 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. |
| `timeout` | Number | | Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds). |
---
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.15.20",
"version": "0.15.29",
"publisher": "sst-dev",
"repository": {
"type": "git",