mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b3a841dd9 | |||
| aca32eaa1c | |||
| 3ae75d7031 | |||
| 4b5e447961 | |||
| 7a2b8eae76 | |||
| d983b9485d | |||
| 14836de276 | |||
| a0f032c9b9 | |||
| e6132fc6a4 | |||
| 950b608c4d | |||
| 3210df7428 | |||
| cdeb82e9ca | |||
| a9cae7b335 | |||
| 972c0893dd | |||
| e5d89ca567 | |||
| 4ae70d4b0d | |||
| 935cd7481b | |||
| 5553efea5e | |||
| 0ff73ed8a6 | |||
| 5e792d7ac5 | |||
| 4a77e94e3c | |||
| 4c563ea405 | |||
| 5875257462 | |||
| 9701891e94 | |||
| a2ab37c1b6 | |||
| 4d6e2d8efc | |||
| 4407d5d96f | |||
| 244945c0e7 | |||
| c652b2b4e8 | |||
| aabeeb1431 | |||
| 0fbedc5e19 | |||
| 12782fff14 | |||
| ca463a2346 | |||
| 7265cdf817 | |||
| 7baa751351 | |||
| 5b86fa9109 | |||
| aa7e008fe1 | |||
| 792664071c | |||
| a0541ba57a | |||
| 4994bf1b46 | |||
| 1e24514d61 | |||
| 4b1c6300a0 | |||
| db3fb9d316 |
@@ -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 }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,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,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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "0.15.20",
|
||||
"version": "0.15.29",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+62
-38
@@ -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}>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "0.15.20",
|
||||
"version": "0.15.29",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "0.15.20",
|
||||
"version": "0.15.29",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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). |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user