Compare commits

...

14 Commits

Author SHA1 Message Date
Adam
fbd5eff77a chore(docs): i18n sync 2026-02-27 19:18:55 -06:00
Alex Yaroshuk
2a2082233d fix(app): display skill name in skill tool call (#15413) 2026-02-27 19:18:14 -06:00
Adam
267d2c82de chore: cleanup 2026-02-27 19:12:19 -06:00
Jay
0b8c1f1f7d docs: Update OpenCode Go subscription and usage details (#15415) 2026-02-27 16:04:53 -08:00
Frank
2eb1d4cb9a doc: go 2026-02-27 18:03:39 -05:00
Frank
d2a8f44c22 doc: opencode go 2026-02-27 17:38:30 -05:00
opencode-agent[bot]
1f1f36aac1 chore: update nix node_modules hashes 2026-02-27 21:59:23 +00:00
Adam
7f851da15e chore(console): i18n sync (#15360) 2026-02-27 15:50:50 -06:00
Adam
a3bdb974b3 chore(app): deps 2026-02-27 15:49:38 -06:00
opencode-agent[bot]
46d678fce9 chore: generate 2026-02-27 21:17:37 +00:00
Alex Yaroshuk
1f2348c1ef fix(app): make bash output selectable (#15378) 2026-02-27 15:16:33 -06:00
shivam kr chaudhary
f347194e31 docs: add missing Bosanski link to Arabic README (#15399) 2026-02-27 15:15:48 -06:00
opencode-agent[bot]
7ff2710ce3 chore: generate 2026-02-27 20:37:28 +00:00
James Long
c12ce2ffff feat(core): basic implementation of remote workspace support (#15120) 2026-02-27 15:36:39 -05:00
244 changed files with 9624 additions and 5468 deletions

View File

@@ -27,6 +27,7 @@
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.bs.md">Bosanski</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |

View File

@@ -545,7 +545,7 @@
"@kobalte/core": "0.13.11",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.13",
"@pierre/diffs": "1.1.0-beta.18",
"@playwright/test": "1.51.0",
"@solid-primitives/storage": "4.3.3",
"@solidjs/meta": "0.29.4",
@@ -1469,7 +1469,9 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="],
"@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=",
"aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=",
"aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=",
"x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg="
"x86_64-linux": "sha256-2XLuizbG90QDUQL+1M90XxfVZxjkIQ1cFYS46nnVO7g=",
"aarch64-linux": "sha256-hlckiGAtbpAlwgcE7KgzKKRq9T2FEOSq3Q1MhuHfZ2c=",
"aarch64-darwin": "sha256-V/8Kay+5bDb/BSVgBQhSMwzmRmkNGl3U0HFMVbVcMak=",
"x86_64-darwin": "sha256-duLDF88Q/hXK5jwBy4dVxMSiTTS0R4obp9MlTuOF/Pw="
}
}

View File

@@ -35,7 +35,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.13",
"@pierre/diffs": "1.1.0-beta.18",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",

View File

@@ -7,9 +7,21 @@ import { Font } from "@opencode-ai/ui/font"
import "@ibm/plex/css/ibm-plex.css"
import "./app.css"
import { LanguageProvider } from "~/context/language"
import { I18nProvider } from "~/context/i18n"
import { I18nProvider, useI18n } from "~/context/i18n"
import { strip } from "~/lib/language"
function AppMeta() {
const i18n = useI18n()
return (
<>
<Title>opencode</Title>
<Meta name="description" content={i18n.t("app.meta.description")} />
<Favicon />
<Font />
</>
)
}
export default function App() {
return (
<Router
@@ -19,10 +31,7 @@ export default function App() {
<LanguageProvider>
<I18nProvider>
<MetaProvider>
<Title>opencode</Title>
<Meta name="description" content="OpenCode - The open source coding agent." />
<Favicon />
<Font />
<AppMeta />
<Suspense>{props.children}</Suspense>
</MetaProvider>
</I18nProvider>

View File

@@ -124,8 +124,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
<section data-component="top">
<div onContextMenu={handleLogoContextMenu}>
<A href={language.route("/")}>
<img data-slot="logo light" src={logoLight} alt="OpenCode" width="189" height="34" />
<img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
<img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
<img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
</A>
</div>

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "الرئيسية",
"nav.openMenu": "فتح القائمة",
"nav.getStartedFree": "ابدأ مجانا",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "نسخ الشعار كـ SVG",
"nav.context.copyWordmark": "نسخ اسم العلامة كـ SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "الوثائق",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "شعار opencode الفاتح",
"notFound.logoDarkAlt": "شعار opencode الداكن",
"user.logout": "تسجيل الخروج",
"auth.callback.error.codeMissing": "لم يتم العثور على رمز التفويض.",
"workspace.select": "اختر مساحة العمل",
"workspace.createNew": "+ إنشاء مساحة عمل جديدة",
"workspace.modal.title": "إنشاء مساحة عمل جديدة",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "يجب أن يكون مبلغ الشحن ${{amount}} على الأقل",
"error.reloadTriggerMin": "يجب أن يكون حد الرصيد ${{amount}} على الأقل",
"app.meta.description": "OpenCode - وكيل البرمجة مفتوح المصدر.",
"home.title": "OpenCode | وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
"temp.title": "opencode | وكيل برمجة بالذكاء الاصطناعي مبني للطرفية",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": "، بما في ذلك النماذج المحلية",
"temp.screenshot.caption": "واجهة OpenCode الطرفية مع سمة tokyonight",
"temp.screenshot.alt": "واجهة OpenCode الطرفية بسمة tokyonight",
"temp.logoLightAlt": "شعار opencode الفاتح",
"temp.logoDarkAlt": "شعار opencode الداكن",
"home.banner.badge": "جديد",
"home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية",
@@ -238,6 +247,24 @@ export const dict = {
"تتم استضافة جميع نماذج Zen في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
"zen.api.error.modelFormatNotSupported": "النموذج {{model}} غير مدعوم للتنسيق {{format}}",
"zen.api.error.noProviderAvailable": "لا يوجد مزود متاح",
"zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم",
"zen.api.error.missingApiKey": "مفتاح API مفقود.",
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"تم تجاوز حصة الاشتراك. يمكنك الاستمرار في استخدام النماذج المجانية.",
"zen.api.error.noPaymentMethod": "لا توجد طريقة دفع. أضف طريقة دفع هنا: {{billingUrl}}",
"zen.api.error.insufficientBalance": "رصيد غير كاف. إدارة فواتيرك هنا: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"وصلت مساحة العمل الخاصة بك إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
"zen.api.error.modelDisabled": "النموذج معطل",
"black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم",
"black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.",
"black.hero.title": "الوصول إلى أفضل نماذج البرمجة في العالم",
@@ -446,6 +473,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع والمحاولة مرة أخرى.",
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
"workspace.reload.retry": "أعد المحاولة",
"workspace.reload.error.paymentFailed": "فشلت عملية الدفع.",
"workspace.payments.title": "سجل المدفوعات",
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
@@ -563,6 +591,10 @@ export const dict = {
"enterprise.form.send": "إرسال",
"enterprise.form.sending": "جارٍ الإرسال...",
"enterprise.form.success": "تم إرسال الرسالة، سنتواصل معك قريبًا.",
"enterprise.form.success.submitted": "تم إرسال النموذج بنجاح.",
"enterprise.form.error.allFieldsRequired": "جميع الحقول مطلوبة.",
"enterprise.form.error.invalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح.",
"enterprise.form.error.internalServer": "خطأ داخلي في الخادم.",
"enterprise.faq.title": "الأسئلة الشائعة",
"enterprise.faq.q1": "ما هو OpenCode Enterprise؟",
"enterprise.faq.a1":
@@ -595,6 +627,7 @@ export const dict = {
"bench.list.table.agent": "الوكيل",
"bench.list.table.model": "النموذج",
"bench.list.table.score": "الدرجة",
"bench.submission.error.allFieldsRequired": "جميع الحقول مطلوبة.",
"bench.detail.title": "المعيار - {{task}}",
"bench.detail.notFound": "المهمة غير موجودة",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Início",
"nav.openMenu": "Abrir menu",
"nav.getStartedFree": "Começar grátis",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Copiar logo como SVG",
"nav.context.copyWordmark": "Copiar marca como SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Documentação",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "logo opencode claro",
"notFound.logoDarkAlt": "logo opencode escuro",
"user.logout": "Sair",
"auth.callback.error.codeMissing": "Nenhum código de autorização encontrado.",
"workspace.select": "Selecionar workspace",
"workspace.createNew": "+ Criar novo workspace",
"workspace.modal.title": "Criar novo workspace",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "O valor de recarga deve ser de pelo menos ${{amount}}",
"error.reloadTriggerMin": "O gatilho de saldo deve ser de pelo menos ${{amount}}",
"app.meta.description": "OpenCode - O agente de codificação de código aberto.",
"home.title": "OpenCode | O agente de codificação de código aberto com IA",
"temp.title": "opencode | Agente de codificação com IA feito para o terminal",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", incluindo modelos locais",
"temp.screenshot.caption": "OpenCode TUI com o tema tokyonight",
"temp.screenshot.alt": "OpenCode TUI com tema tokyonight",
"temp.logoLightAlt": "logo opencode claro",
"temp.logoDarkAlt": "logo opencode escuro",
"home.banner.badge": "Novo",
"home.banner.text": "App desktop disponível em beta",
@@ -242,6 +251,24 @@ export const dict = {
"Todos os modelos Zen são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelo, com as",
"zen.privacy.exceptionsLink": "seguintes exceções",
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} não suportado para o formato {{format}}",
"zen.api.error.noProviderAvailable": "Nenhum provedor disponível",
"zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado",
"zen.api.error.missingApiKey": "Chave de API ausente.",
"zen.api.error.invalidApiKey": "Chave de API inválida.",
"zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Cota de assinatura excedida. Você pode continuar usando modelos gratuitos.",
"zen.api.error.noPaymentMethod": "Nenhuma forma de pagamento. Adicione uma forma de pagamento aqui: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gerencie seu faturamento aqui: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Seu workspace atingiu o limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
"zen.api.error.modelDisabled": "O modelo está desabilitado",
"black.meta.title": "OpenCode Black | Acesse os melhores modelos de codificação do mundo",
"black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.",
"black.hero.title": "Acesse os melhores modelos de codificação do mundo",
@@ -451,6 +478,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Por favor, atualize sua forma de pagamento e tente novamente.",
"workspace.reload.retrying": "Tentando novamente...",
"workspace.reload.retry": "Tentar novamente",
"workspace.reload.error.paymentFailed": "Pagamento falhou.",
"workspace.payments.title": "Histórico de Pagamentos",
"workspace.payments.subtitle": "Transações de pagamento recentes.",
@@ -571,6 +599,10 @@ export const dict = {
"enterprise.form.send": "Enviar",
"enterprise.form.sending": "Enviando...",
"enterprise.form.success": "Mensagem enviada, entraremos em contato em breve.",
"enterprise.form.success.submitted": "Formulário enviado com sucesso.",
"enterprise.form.error.allFieldsRequired": "Todos os campos são obrigatórios.",
"enterprise.form.error.invalidEmailFormat": "Formato de e-mail inválido.",
"enterprise.form.error.internalServer": "Erro interno do servidor.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "O que é OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -603,6 +635,7 @@ export const dict = {
"bench.list.table.agent": "Agente",
"bench.list.table.model": "Modelo",
"bench.list.table.score": "Pontuação",
"bench.submission.error.allFieldsRequired": "Todos os campos são obrigatórios.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Tarefa não encontrada",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Hjem",
"nav.openMenu": "Åbn menu",
"nav.getStartedFree": "Kom i gang gratis",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Kopier logo som SVG",
"nav.context.copyWordmark": "Kopier wordmark som SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Dokumentation",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo light",
"notFound.logoDarkAlt": "opencode logo dark",
"user.logout": "Log ud",
"auth.callback.error.codeMissing": "Ingen autorisationskode fundet.",
"workspace.select": "Vælg workspace",
"workspace.createNew": "+ Opret nyt workspace",
"workspace.modal.title": "Opret nyt workspace",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "Genopfyldningsbeløb skal være mindst ${{amount}}",
"error.reloadTriggerMin": "Saldogrænse skal være mindst ${{amount}}",
"app.meta.description": "OpenCode - Den open source kodningsagent.",
"home.title": "OpenCode | Den open source AI-kodningsagent",
"temp.title": "opencode | AI-kodningsagent bygget til terminalen",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", inklusive lokale modeller",
"temp.screenshot.caption": "opencode TUI med tokyonight-temaet",
"temp.screenshot.alt": "opencode TUI med tokyonight-temaet",
"temp.logoLightAlt": "opencode logo light",
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Ny",
"home.banner.text": "Desktop-app tilgængelig i beta",
@@ -240,6 +249,24 @@ export const dict = {
"Alle Zen-modeller er hostet i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning med",
"zen.privacy.exceptionsLink": "følgende undtagelser",
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
"zen.api.error.modelFormatNotSupported": "Model {{model}} understøttes ikke for format {{format}}",
"zen.api.error.noProviderAvailable": "Ingen udbyder tilgængelig",
"zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke",
"zen.api.error.missingApiKey": "Manglende API-nøgle.",
"zen.api.error.invalidApiKey": "Ugyldig API-nøgle.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Abonnementskvote overskredet. Du kan fortsætte med at bruge gratis modeller.",
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Tilføj en betalingsmetode her: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Utilstrækkelig saldo. Administrer din fakturering her: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Dit workspace har nået sin månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
"black.meta.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller",
"black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.",
"black.hero.title": "Få adgang til verdens bedste kodningsmodeller",
@@ -449,6 +476,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
"workspace.reload.retrying": "Prøver igen...",
"workspace.reload.retry": "Prøv igen",
"workspace.reload.error.paymentFailed": "Betaling mislykkedes.",
"workspace.payments.title": "Betalingshistorik",
"workspace.payments.subtitle": "Seneste betalingstransaktioner.",
@@ -567,6 +595,10 @@ export const dict = {
"enterprise.form.send": "Send",
"enterprise.form.sending": "Sender...",
"enterprise.form.success": "Besked sendt, vi vender tilbage snart.",
"enterprise.form.success.submitted": "Formular indsendt med succes.",
"enterprise.form.error.allFieldsRequired": "Alle felter er påkrævet.",
"enterprise.form.error.invalidEmailFormat": "Ugyldigt e-mailformat.",
"enterprise.form.error.internalServer": "Intern serverfejl.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Hvad er OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -599,6 +631,7 @@ export const dict = {
"bench.list.table.agent": "Agent",
"bench.list.table.model": "Model",
"bench.list.table.score": "Score",
"bench.submission.error.allFieldsRequired": "Alle felter er påkrævet.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Opgave ikke fundet",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Startseite",
"nav.openMenu": "Menü öffnen",
"nav.getStartedFree": "Kostenlos starten",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Logo als SVG kopieren",
"nav.context.copyWordmark": "Wortmarke als SVG kopieren",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Dokumentation",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "OpenCode Logo hell",
"notFound.logoDarkAlt": "OpenCode Logo dunkel",
"user.logout": "Abmelden",
"auth.callback.error.codeMissing": "Kein Autorisierungscode gefunden.",
"workspace.select": "Workspace auswählen",
"workspace.createNew": "+ Neuen Workspace erstellen",
"workspace.modal.title": "Neuen Workspace erstellen",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "Aufladebetrag muss mindestens ${{amount}} betragen",
"error.reloadTriggerMin": "Guthaben-Auslöser muss mindestens ${{amount}} betragen",
"app.meta.description": "OpenCode - Der Open-Source Coding-Agent.",
"home.title": "OpenCode | Der Open-Source AI-Coding-Agent",
"temp.title": "OpenCode | Für das Terminal gebauter AI-Coding-Agent",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", einschließlich lokaler Modelle",
"temp.screenshot.caption": "OpenCode TUI mit dem Tokyonight-Theme",
"temp.screenshot.alt": "OpenCode TUI mit Tokyonight-Theme",
"temp.logoLightAlt": "OpenCode Logo hell",
"temp.logoDarkAlt": "OpenCode Logo dunkel",
"home.banner.badge": "Neu",
"home.banner.text": "Desktop-App in der Beta verfügbar",
@@ -242,6 +251,24 @@ export const dict = {
"Alle Zen-Modelle werden in den USA gehostet. Anbieter folgen einer Zero-Retention-Policy und nutzen deine Daten nicht für Modelltraining, mit den",
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
"zen.api.error.modelFormatNotSupported": "Modell {{model}} wird für das Format {{format}} nicht unterstützt",
"zen.api.error.noProviderAvailable": "Kein Anbieter verfügbar",
"zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt",
"zen.api.error.missingApiKey": "Fehlender API-Key.",
"zen.api.error.invalidApiKey": "Ungültiger API-Key.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Abonnement-Quote überschritten. Du kannst weiterhin kostenlose Modelle nutzen.",
"zen.api.error.noPaymentMethod": "Keine Zahlungsmethode. Füge hier eine Zahlungsmethode hinzu: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Unzureichendes Guthaben. Verwalte deine Abrechnung hier: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Dein Workspace hat sein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
"black.meta.title": "OpenCode Black | Zugriff auf die weltweit besten Coding-Modelle",
"black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.",
"black.hero.title": "Zugriff auf die weltweit besten Coding-Modelle",
@@ -451,6 +478,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Bitte aktualisiere deine Zahlungsmethode und versuche es erneut.",
"workspace.reload.retrying": "Versuche erneut...",
"workspace.reload.retry": "Erneut versuchen",
"workspace.reload.error.paymentFailed": "Zahlung fehlgeschlagen.",
"workspace.payments.title": "Zahlungshistorie",
"workspace.payments.subtitle": "Kürzliche Zahlungstransaktionen.",
@@ -571,6 +599,10 @@ export const dict = {
"enterprise.form.send": "Senden",
"enterprise.form.sending": "Sende...",
"enterprise.form.success": "Nachricht gesendet, wir melden uns bald.",
"enterprise.form.success.submitted": "Formular erfolgreich gesendet.",
"enterprise.form.error.allFieldsRequired": "Alle Felder sind erforderlich.",
"enterprise.form.error.invalidEmailFormat": "Ungültiges E-Mail-Format.",
"enterprise.form.error.internalServer": "Interner Serverfehler.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Was ist OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -603,6 +635,7 @@ export const dict = {
"bench.list.table.agent": "Agent",
"bench.list.table.model": "Modell",
"bench.list.table.score": "Score",
"bench.submission.error.allFieldsRequired": "Alle Felder sind erforderlich.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Task nicht gefunden",

View File

@@ -11,6 +11,7 @@ export const dict = {
"nav.home": "Home",
"nav.openMenu": "Open menu",
"nav.getStartedFree": "Get started for free",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Copy logo as SVG",
"nav.context.copyWordmark": "Copy wordmark as SVG",
@@ -38,9 +39,13 @@ export const dict = {
"notFound.docs": "Docs",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo light",
"notFound.logoDarkAlt": "opencode logo dark",
"user.logout": "Logout",
"auth.callback.error.codeMissing": "No authorization code found.",
"workspace.select": "Select workspace",
"workspace.createNew": "+ Create New Workspace",
"workspace.modal.title": "Create New Workspace",
@@ -72,6 +77,8 @@ export const dict = {
"error.reloadAmountMin": "Reload amount must be at least ${{amount}}",
"error.reloadTriggerMin": "Balance trigger must be at least ${{amount}}",
"app.meta.description": "OpenCode - The open source coding agent.",
"home.title": "OpenCode | The open source AI coding agent",
"temp.title": "opencode | AI coding agent built for the terminal",
@@ -87,6 +94,8 @@ export const dict = {
"temp.feature.models.afterLink": ", including local models",
"temp.screenshot.caption": "opencode TUI with the tokyonight theme",
"temp.screenshot.alt": "opencode TUI with tokyonight theme",
"temp.logoLightAlt": "opencode logo light",
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "New",
"home.banner.text": "Desktop app available in beta",
@@ -234,6 +243,24 @@ export const dict = {
"All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
"zen.privacy.exceptionsLink": "following exceptions",
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
"zen.api.error.modelNotSupported": "Model {{model}} not supported",
"zen.api.error.modelFormatNotSupported": "Model {{model}} not supported for format {{format}}",
"zen.api.error.noProviderAvailable": "No provider available",
"zen.api.error.providerNotSupported": "Provider {{provider}} not supported",
"zen.api.error.missingApiKey": "Missing API key.",
"zen.api.error.invalidApiKey": "Invalid API key.",
"zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Subscription quota exceeded. You can continue using free models.",
"zen.api.error.noPaymentMethod": "No payment method. Add a payment method here: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Insufficient balance. Manage your billing here: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Your workspace has reached its monthly spending limit of ${{amount}}. Manage your limits here: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model is disabled",
"black.meta.title": "OpenCode Black | Access all the world's best coding models",
"black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.",
"black.hero.title": "Access all the world's best coding models",
@@ -443,6 +470,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Please update your payment method and try again.",
"workspace.reload.retrying": "Retrying...",
"workspace.reload.retry": "Retry",
"workspace.reload.error.paymentFailed": "Payment failed.",
"workspace.payments.title": "Payments History",
"workspace.payments.subtitle": "Recent payment transactions.",
@@ -561,6 +589,10 @@ export const dict = {
"enterprise.form.send": "Send",
"enterprise.form.sending": "Sending...",
"enterprise.form.success": "Message sent, we'll be in touch soon.",
"enterprise.form.success.submitted": "Form submitted successfully.",
"enterprise.form.error.allFieldsRequired": "All fields are required.",
"enterprise.form.error.invalidEmailFormat": "Invalid email format.",
"enterprise.form.error.internalServer": "Internal server error.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "What is OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -593,6 +625,7 @@ export const dict = {
"bench.list.table.agent": "Agent",
"bench.list.table.model": "Model",
"bench.list.table.score": "Score",
"bench.submission.error.allFieldsRequired": "All fields are required.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Task not found",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Inicio",
"nav.openMenu": "Abrir menú",
"nav.getStartedFree": "Empezar gratis",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Copiar logo como SVG",
"nav.context.copyWordmark": "Copiar marca como SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Documentación",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo claro",
"notFound.logoDarkAlt": "opencode logo oscuro",
"user.logout": "Cerrar sesión",
"auth.callback.error.codeMissing": "No se encontró código de autorización.",
"workspace.select": "Seleccionar espacio de trabajo",
"workspace.createNew": "+ Crear nuevo espacio de trabajo",
"workspace.modal.title": "Crear nuevo espacio de trabajo",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "La cantidad de recarga debe ser al menos ${{amount}}",
"error.reloadTriggerMin": "El disparador de saldo debe ser al menos ${{amount}}",
"app.meta.description": "OpenCode - El agente de codificación de código abierto.",
"home.title": "OpenCode | El agente de codificación IA de código abierto",
"temp.title": "opencode | Agente de codificación IA creado para la terminal",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", incluyendo modelos locales",
"temp.screenshot.caption": "opencode TUI con el tema tokyonight",
"temp.screenshot.alt": "opencode TUI con tema tokyonight",
"temp.logoLightAlt": "logo de opencode claro",
"temp.logoDarkAlt": "logo de opencode oscuro",
"home.banner.badge": "Nuevo",
"home.banner.text": "Aplicación de escritorio disponible en beta",
@@ -243,6 +252,24 @@ export const dict = {
"Todos los modelos Zen están alojados en EE. UU. Los proveedores siguen una política de cero retención y no usan tus datos para entrenamiento de modelos, con las",
"zen.privacy.exceptionsLink": "siguientes excepciones",
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} no soportado para el formato {{format}}",
"zen.api.error.noProviderAvailable": "Ningún proveedor disponible",
"zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado",
"zen.api.error.missingApiKey": "Falta la clave API.",
"zen.api.error.invalidApiKey": "Clave API inválida.",
"zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Cuota de suscripción excedida. Puedes continuar usando modelos gratuitos.",
"zen.api.error.noPaymentMethod": "Sin método de pago. Añade un método de pago aquí: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gestiona tu facturación aquí: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Tu espacio de trabajo ha alcanzado su límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
"black.meta.title": "OpenCode Black | Accede a los mejores modelos de codificación del mundo",
"black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.",
"black.hero.title": "Accede a los mejores modelos de codificación del mundo",
@@ -452,6 +479,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Por favor actualiza tu método de pago e intenta de nuevo.",
"workspace.reload.retrying": "Reintentando...",
"workspace.reload.retry": "Reintentar",
"workspace.reload.error.paymentFailed": "El pago falló.",
"workspace.payments.title": "Historial de Pagos",
"workspace.payments.subtitle": "Transacciones de pago recientes.",
@@ -571,6 +599,10 @@ export const dict = {
"enterprise.form.send": "Enviar",
"enterprise.form.sending": "Enviando...",
"enterprise.form.success": "Mensaje enviado, estaremos en contacto pronto.",
"enterprise.form.success.submitted": "Formulario enviado con éxito.",
"enterprise.form.error.allFieldsRequired": "Todos los campos son obligatorios.",
"enterprise.form.error.invalidEmailFormat": "Formato de correo inválido.",
"enterprise.form.error.internalServer": "Error interno del servidor.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "¿Qué es OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -603,6 +635,7 @@ export const dict = {
"bench.list.table.agent": "Agente",
"bench.list.table.model": "Modelo",
"bench.list.table.score": "Puntuación",
"bench.submission.error.allFieldsRequired": "Todos los campos son obligatorios.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Tarea no encontrada",

View File

@@ -3,6 +3,7 @@ import { dict as en } from "./en"
export const dict = {
...en,
"app.meta.description": "OpenCode - L'agent de code open source.",
"nav.github": "GitHub",
"nav.docs": "Documentation",
"nav.changelog": "Changelog",
@@ -15,6 +16,7 @@ export const dict = {
"nav.home": "Accueil",
"nav.openMenu": "Ouvrir le menu",
"nav.getStartedFree": "Commencer gratuitement",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Copier le logo en SVG",
"nav.context.copyWordmark": "Copier le logotype en SVG",
@@ -42,6 +44,8 @@ export const dict = {
"notFound.docs": "Documentation",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo light",
"notFound.logoDarkAlt": "opencode logo dark",
"user.logout": "Se déconnecter",
@@ -75,6 +79,7 @@ export const dict = {
"error.modelRequired": "Le modèle est requis",
"error.reloadAmountMin": "Le montant de recharge doit être d'au moins {{amount}} $",
"error.reloadTriggerMin": "Le seuil de déclenchement doit être d'au moins {{amount}} $",
"auth.callback.error.codeMissing": "Aucun code d'autorisation trouvé.",
"home.title": "OpenCode | L'agent de code IA open source",
@@ -91,6 +96,8 @@ export const dict = {
"temp.feature.models.afterLink": ", y compris les modèles locaux",
"temp.screenshot.caption": "OpenCode TUI avec le thème tokyonight",
"temp.screenshot.alt": "OpenCode TUI avec le thème tokyonight",
"temp.logoLightAlt": "opencode logo light",
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Nouveau",
"home.banner.text": "Application desktop disponible en bêta",
@@ -246,6 +253,24 @@ export const dict = {
"Tous les modèles Zen sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
"zen.privacy.exceptionsLink": "exceptions suivantes",
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
"zen.api.error.modelFormatNotSupported": "Modèle {{model}} non pris en charge pour le format {{format}}",
"zen.api.error.noProviderAvailable": "Aucun fournisseur disponible",
"zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge",
"zen.api.error.missingApiKey": "Clé API manquante.",
"zen.api.error.invalidApiKey": "Clé API invalide.",
"zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Quota d'abonnement dépassé. Vous pouvez continuer à utiliser les modèles gratuits.",
"zen.api.error.noPaymentMethod": "Aucune méthode de paiement. Ajoutez une méthode de paiement ici : {{billingUrl}}",
"zen.api.error.insufficientBalance": "Solde insuffisant. Gérez votre facturation ici : {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Votre espace de travail a atteint sa limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
"zen.api.error.modelDisabled": "Le modèle est désactivé",
"black.meta.title": "OpenCode Black | Accédez aux meilleurs modèles de code au monde",
"black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.",
"black.hero.title": "Accédez aux meilleurs modèles de code au monde",
@@ -457,6 +482,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Veuillez mettre à jour votre méthode de paiement et réessayer.",
"workspace.reload.retrying": "Nouvelle tentative...",
"workspace.reload.retry": "Réessayer",
"workspace.reload.error.paymentFailed": "Échec du paiement.",
"workspace.payments.title": "Historique des paiements",
"workspace.payments.subtitle": "Transactions de paiement récentes.",
@@ -581,6 +607,10 @@ export const dict = {
"enterprise.form.send": "Envoyer",
"enterprise.form.sending": "Envoi...",
"enterprise.form.success": "Message envoyé, nous vous contacterons bientôt.",
"enterprise.form.success.submitted": "Formulaire soumis avec succès.",
"enterprise.form.error.allFieldsRequired": "Tous les champs sont requis.",
"enterprise.form.error.invalidEmailFormat": "Format d'e-mail invalide.",
"enterprise.form.error.internalServer": "Erreur interne du serveur.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Qu'est-ce que OpenCode Enterprise ?",
"enterprise.faq.a1":
@@ -640,4 +670,5 @@ export const dict = {
"bench.detail.table.duration": "Durée",
"bench.detail.run.title": "Exécution {{n}}",
"bench.detail.rawJson": "JSON brut",
"bench.submission.error.allFieldsRequired": "Tous les champs sont requis.",
} satisfies Dict

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Home",
"nav.openMenu": "Apri menu",
"nav.getStartedFree": "Inizia gratis",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Copia il logo come SVG",
"nav.context.copyWordmark": "Copia il wordmark come SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Documentazione",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "logo chiaro di opencode",
"notFound.logoDarkAlt": "logo scuro di opencode",
"user.logout": "Esci",
"auth.callback.error.codeMissing": "Nessun codice di autorizzazione trovato.",
"workspace.select": "Seleziona workspace",
"workspace.createNew": "+ Crea nuovo workspace",
"workspace.modal.title": "Crea nuovo workspace",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "L'importo della ricarica deve essere almeno ${{amount}}",
"error.reloadTriggerMin": "La soglia del saldo deve essere almeno ${{amount}}",
"app.meta.description": "OpenCode - L'agente di programmazione open source.",
"home.title": "OpenCode | L'agente di coding IA open source",
"temp.title": "opencode | Agente di coding IA costruito per il terminale",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", inclusi modelli locali",
"temp.screenshot.caption": "OpenCode TUI con il tema tokyonight",
"temp.screenshot.alt": "OpenCode TUI con tema tokyonight",
"temp.logoLightAlt": "logo chiaro di opencode",
"temp.logoDarkAlt": "logo scuro di opencode",
"home.banner.badge": "Nuovo",
"home.banner.text": "App desktop disponibile in beta",
@@ -240,6 +249,24 @@ export const dict = {
"Tutti i modelli Zen sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
"zen.privacy.exceptionsLink": "seguenti eccezioni",
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
"zen.api.error.modelFormatNotSupported": "Modello {{model}} non supportato per il formato {{format}}",
"zen.api.error.noProviderAvailable": "Nessun provider disponibile",
"zen.api.error.providerNotSupported": "Provider {{provider}} non supportato",
"zen.api.error.missingApiKey": "Chiave API mancante.",
"zen.api.error.invalidApiKey": "Chiave API non valida.",
"zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Quota dell'abbonamento superata. Puoi continuare a utilizzare modelli gratuiti.",
"zen.api.error.noPaymentMethod": "Nessun metodo di pagamento. Aggiungi un metodo di pagamento qui: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Saldo insufficiente. Gestisci la tua fatturazione qui: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"La tua area di lavoro ha raggiunto il limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
"zen.api.error.modelDisabled": "Il modello è disabilitato",
"black.meta.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo",
"black.meta.description":
"Ottieni l'accesso a Claude, GPT, Gemini e altri con i piani di abbonamento OpenCode Black.",
@@ -451,6 +478,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Aggiorna il tuo metodo di pagamento e riprova.",
"workspace.reload.retrying": "Riprovo...",
"workspace.reload.retry": "Riprova",
"workspace.reload.error.paymentFailed": "Pagamento fallito.",
"workspace.payments.title": "Cronologia Pagamenti",
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
@@ -569,6 +597,10 @@ export const dict = {
"enterprise.form.send": "Invia",
"enterprise.form.sending": "Invio...",
"enterprise.form.success": "Messaggio inviato, ti contatteremo presto.",
"enterprise.form.success.submitted": "Modulo inviato con successo.",
"enterprise.form.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
"enterprise.form.error.invalidEmailFormat": "Formato email non valido.",
"enterprise.form.error.internalServer": "Errore interno del server.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Cos'è OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -601,6 +633,7 @@ export const dict = {
"bench.list.table.agent": "Agente",
"bench.list.table.model": "Modello",
"bench.list.table.score": "Punteggio",
"bench.submission.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Task non trovato",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "ホーム",
"nav.openMenu": "メニューを開く",
"nav.getStartedFree": "無料ではじめる",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "ロゴをSVGでコピー",
"nav.context.copyWordmark": "ワードマークをSVGでコピー",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "ドキュメント",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencodeのロゴライト",
"notFound.logoDarkAlt": "opencodeのロゴダーク",
"user.logout": "ログアウト",
"auth.callback.error.codeMissing": "認証コードが見つかりません。",
"workspace.select": "ワークスペースを選択",
"workspace.createNew": "+ 新しいワークスペースを作成",
"workspace.modal.title": "新しいワークスペースを作成",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "リロード額は少なくとも ${{amount}} である必要があります",
"error.reloadTriggerMin": "残高トリガーは少なくとも ${{amount}} である必要があります",
"app.meta.description": "OpenCode - オープンソースのコーディングエージェント。",
"home.title": "OpenCode | オープンソースのAIコーディングエージェント",
"temp.title": "OpenCode | ターミナル向けに構築されたAIコーディングエージェント",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": "を通じて75以上のLLMプロバイダーをサポート",
"temp.screenshot.caption": "tokyonight テーマを使用した OpenCode TUI",
"temp.screenshot.alt": "tokyonight テーマの OpenCode TUI",
"temp.logoLightAlt": "opencodeのロゴライト",
"temp.logoDarkAlt": "opencodeのロゴダーク",
"home.banner.badge": "新着",
"home.banner.text": "デスクトップアプリのベータ版が利用可能",
@@ -239,6 +248,25 @@ export const dict = {
"すべてのZenモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません",
"zen.privacy.exceptionsLink": "以下の例外",
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
"zen.api.error.modelFormatNotSupported": "フォーマット {{format}} ではモデル {{model}} はサポートされていません",
"zen.api.error.noProviderAvailable": "利用可能なプロバイダーがありません",
"zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません",
"zen.api.error.missingApiKey": "APIキーがありません。",
"zen.api.error.invalidApiKey": "無効なAPIキーです。",
"zen.api.error.subscriptionQuotaExceeded":
"サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"サブスクリプションの制限を超えました。無料モデルは引き続きご利用いただけます。",
"zen.api.error.noPaymentMethod": "お支払い方法がありません。こちらからお支払い方法を追加してください: {{billingUrl}}",
"zen.api.error.insufficientBalance": "残高が不足しています。こちらから請求を管理してください: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"ワークスペースが月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
"zen.api.error.modelDisabled": "モデルが無効です",
"black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス",
"black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。",
"black.hero.title": "世界最高峰のコーディングモデルすべてにアクセス",
@@ -448,6 +476,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "支払い方法を更新して、もう一度お試しください。",
"workspace.reload.retrying": "再試行中...",
"workspace.reload.retry": "再試行",
"workspace.reload.error.paymentFailed": "支払いに失敗しました。",
"workspace.payments.title": "支払い履歴",
"workspace.payments.subtitle": "最近の支払い取引。",
@@ -568,6 +597,10 @@ export const dict = {
"enterprise.form.send": "送信",
"enterprise.form.sending": "送信中...",
"enterprise.form.success": "送信しました。まもなくご連絡いたします。",
"enterprise.form.success.submitted": "フォームが正常に送信されました。",
"enterprise.form.error.allFieldsRequired": "すべての項目は必須です。",
"enterprise.form.error.invalidEmailFormat": "無効なメール形式です。",
"enterprise.form.error.internalServer": "内部サーバーエラー。",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "OpenCode Enterpriseとは",
"enterprise.faq.a1":
@@ -600,6 +633,7 @@ export const dict = {
"bench.list.table.agent": "エージェント",
"bench.list.table.model": "モデル",
"bench.list.table.score": "スコア",
"bench.submission.error.allFieldsRequired": "すべての項目は必須です。",
"bench.detail.title": "ベンチマーク - {{task}}",
"bench.detail.notFound": "タスクが見つかりません",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "홈",
"nav.openMenu": "메뉴 열기",
"nav.getStartedFree": "무료로 시작하기",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "로고를 SVG로 복사",
"nav.context.copyWordmark": "워드마크를 SVG로 복사",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "문서",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode 밝은 로고",
"notFound.logoDarkAlt": "opencode 어두운 로고",
"user.logout": "로그아웃",
"auth.callback.error.codeMissing": "인증 코드를 찾을 수 없습니다.",
"workspace.select": "워크스페이스 선택",
"workspace.createNew": "+ 새 워크스페이스 만들기",
"workspace.modal.title": "새 워크스페이스 만들기",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "충전 금액은 최소 ${{amount}}이어야 합니다",
"error.reloadTriggerMin": "잔액 트리거는 최소 ${{amount}}이어야 합니다",
"app.meta.description": "OpenCode - 오픈 소스 코딩 에이전트.",
"home.title": "OpenCode | 오픈 소스 AI 코딩 에이전트",
"temp.title": "OpenCode | 터미널을 위해 만들어진 AI 코딩 에이전트",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": "를 통해 75개 이상의 LLM 제공자 지원",
"temp.screenshot.caption": "tokyonight 테마가 적용된 OpenCode TUI",
"temp.screenshot.alt": "tokyonight 테마가 적용된 OpenCode TUI",
"temp.logoLightAlt": "opencode 밝은 로고",
"temp.logoDarkAlt": "opencode 어두운 로고",
"home.banner.badge": "신규",
"home.banner.text": "데스크톱 앱 베타 버전 출시",
@@ -236,6 +245,24 @@ export const dict = {
"모든 Zen 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
"zen.privacy.exceptionsLink": "다음 예외",
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
"zen.api.error.modelFormatNotSupported": "{{model}} 모델은 {{format}} 형식에 대해 지원되지 않습니다",
"zen.api.error.noProviderAvailable": "사용 가능한 제공자가 없습니다",
"zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다",
"zen.api.error.missingApiKey": "API 키가 누락되었습니다.",
"zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.",
"zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"구독 할당량을 초과했습니다. 무료 모델은 계속 사용할 수 있습니다.",
"zen.api.error.noPaymentMethod": "결제 수단이 없습니다. 결제 수단을 추가하세요: {{billingUrl}}",
"zen.api.error.insufficientBalance": "잔액이 부족합니다. 결제 관리를 여기서 하세요: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"워크스페이스의 월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
"black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요",
"black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.",
"black.hero.title": "세계 최고의 코딩 모델에 액세스하세요",
@@ -445,6 +472,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "결제 수단을 업데이트하고 다시 시도해 주세요.",
"workspace.reload.retrying": "재시도 중...",
"workspace.reload.retry": "재시도",
"workspace.reload.error.paymentFailed": "결제에 실패했습니다.",
"workspace.payments.title": "결제 내역",
"workspace.payments.subtitle": "최근 결제 거래 내역입니다.",
@@ -562,6 +590,10 @@ export const dict = {
"enterprise.form.send": "전송",
"enterprise.form.sending": "전송 중...",
"enterprise.form.success": "메시지가 전송되었습니다. 곧 연락드리겠습니다.",
"enterprise.form.success.submitted": "양식이 성공적으로 제출되었습니다.",
"enterprise.form.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
"enterprise.form.error.invalidEmailFormat": "유효하지 않은 이메일 형식입니다.",
"enterprise.form.error.internalServer": "내부 서버 오류입니다.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "OpenCode 엔터프라이즈란 무엇인가요?",
"enterprise.faq.a1":
@@ -594,6 +626,7 @@ export const dict = {
"bench.list.table.agent": "에이전트",
"bench.list.table.model": "모델",
"bench.list.table.score": "점수",
"bench.submission.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
"bench.detail.title": "벤치마크 - {{task}}",
"bench.detail.notFound": "태스크를 찾을 수 없음",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Hjem",
"nav.openMenu": "Åpne meny",
"nav.getStartedFree": "Kom i gang gratis",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Kopier logo som SVG",
"nav.context.copyWordmark": "Kopier wordmark som SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Dokumentasjon",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo lys",
"notFound.logoDarkAlt": "opencode logo mørk",
"user.logout": "Logg ut",
"auth.callback.error.codeMissing": "Ingen autorisasjonskode funnet.",
"workspace.select": "Velg arbeidsområde",
"workspace.createNew": "+ Opprett nytt arbeidsområde",
"workspace.modal.title": "Opprett nytt arbeidsområde",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "Påfyllingsbeløp må være minst ${{amount}}",
"error.reloadTriggerMin": "Saldo-trigger må være minst ${{amount}}",
"app.meta.description": "OpenCode - Den åpne kildekode kodingsagenten.",
"home.title": "OpenCode | Den åpne kildekode AI-kodingsagenten",
"temp.title": "opencode | AI-kodingsagent bygget for terminalen",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", inkludert lokale modeller",
"temp.screenshot.caption": "opencode TUI med tokyonight-tema",
"temp.screenshot.alt": "opencode TUI med tokyonight-tema",
"temp.logoLightAlt": "opencode logo lys",
"temp.logoDarkAlt": "opencode logo mørk",
"home.banner.badge": "Ny",
"home.banner.text": "Desktop-app tilgjengelig i beta",
@@ -240,6 +249,24 @@ export const dict = {
"Alle Zen-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
"zen.privacy.exceptionsLink": "følgende unntak",
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
"zen.api.error.modelFormatNotSupported": "Modell {{model}} støttes ikke for format {{format}}",
"zen.api.error.noProviderAvailable": "Ingen leverandør tilgjengelig",
"zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke",
"zen.api.error.missingApiKey": "Mangler API-nøkkel.",
"zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.",
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Abonnementskvote overskredet. Du kan fortsette å bruke gratis modeller.",
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Legg til en betalingsmetode her: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Utilstrekkelig saldo. Administrer faktureringen din her: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Arbeidsområdet ditt har nådd sin månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modellen er deaktivert",
"black.meta.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller",
"black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.",
"black.hero.title": "Få tilgang til verdens beste kodemodeller",
@@ -449,6 +476,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Vennligst oppdater betalingsmetoden din og prøv på nytt.",
"workspace.reload.retrying": "Prøver på nytt...",
"workspace.reload.retry": "Prøv på nytt",
"workspace.reload.error.paymentFailed": "Betaling mislyktes.",
"workspace.payments.title": "Betalingshistorikk",
"workspace.payments.subtitle": "Nylige betalingstransaksjoner.",
@@ -567,6 +595,10 @@ export const dict = {
"enterprise.form.send": "Send",
"enterprise.form.sending": "Sender...",
"enterprise.form.success": "Melding sendt, vi tar kontakt snart.",
"enterprise.form.success.submitted": "Skjemaet ble sendt inn.",
"enterprise.form.error.allFieldsRequired": "Alle felt er obligatoriske.",
"enterprise.form.error.invalidEmailFormat": "Ugyldig e-postformat.",
"enterprise.form.error.internalServer": "Intern serverfeil.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Hva er OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -599,6 +631,7 @@ export const dict = {
"bench.list.table.agent": "Agent",
"bench.list.table.model": "Modell",
"bench.list.table.score": "Poengsum",
"bench.submission.error.allFieldsRequired": "Alle felt er obligatoriske.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Oppgave ikke funnet",

View File

@@ -14,6 +14,7 @@ export const dict = {
"nav.home": "Strona główna",
"nav.openMenu": "Otwórz menu",
"nav.getStartedFree": "Zacznij za darmo",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Skopiuj logo jako SVG",
"nav.context.copyWordmark": "Skopiuj logotyp jako SVG",
@@ -41,9 +42,13 @@ export const dict = {
"notFound.docs": "Dokumentacja",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "jasne logo opencode",
"notFound.logoDarkAlt": "ciemne logo opencode",
"user.logout": "Wyloguj się",
"auth.callback.error.codeMissing": "Nie znaleziono kodu autoryzacji.",
"workspace.select": "Wybierz obszar roboczy",
"workspace.createNew": "+ Utwórz nowy obszar roboczy",
"workspace.modal.title": "Utwórz nowy obszar roboczy",
@@ -75,6 +80,8 @@ export const dict = {
"error.reloadAmountMin": "Kwota doładowania musi wynosić co najmniej ${{amount}}",
"error.reloadTriggerMin": "Próg salda musi wynosić co najmniej ${{amount}}",
"app.meta.description": "OpenCode - Otwartoźródłowy agent programistyczny.",
"home.title": "OpenCode | Open source'owy agent AI do kodowania",
"temp.title": "opencode | Agent AI do kodowania zbudowany dla terminala",
@@ -90,6 +97,8 @@ export const dict = {
"temp.feature.models.afterLink": ", w tym modele lokalne",
"temp.screenshot.caption": "OpenCode TUI z motywem tokyonight",
"temp.screenshot.alt": "OpenCode TUI z motywem tokyonight",
"temp.logoLightAlt": "jasne logo opencode",
"temp.logoDarkAlt": "ciemne logo opencode",
"home.banner.badge": "Nowość",
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
@@ -241,6 +250,24 @@ export const dict = {
"Wszystkie modele Zen są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli, z",
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
"zen.api.error.modelFormatNotSupported": "Model {{model}} nie jest obsługiwany dla formatu {{format}}",
"zen.api.error.noProviderAvailable": "Brak dostępnego dostawcy",
"zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany",
"zen.api.error.missingApiKey": "Brak klucza API.",
"zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.",
"zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Przekroczono limit subskrypcji. Możesz kontynuować korzystanie z darmowych modeli.",
"zen.api.error.noPaymentMethod": "Brak metody płatności. Dodaj metodę płatności tutaj: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Niewystarczające saldo. Zarządzaj swoimi płatnościami tutaj: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Twoja przestrzeń robocza osiągnęła miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model jest wyłączony",
"black.meta.title": "OpenCode Black | Dostęp do najlepszych na świecie modeli kodujących",
"black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.",
"black.hero.title": "Dostęp do najlepszych na świecie modeli kodujących",
@@ -450,6 +477,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Zaktualizuj metodę płatności i spróbuj ponownie.",
"workspace.reload.retrying": "Ponawianie...",
"workspace.reload.retry": "Spróbuj ponownie",
"workspace.reload.error.paymentFailed": "Płatność nie powiodła się.",
"workspace.payments.title": "Historia płatności",
"workspace.payments.subtitle": "Ostatnie transakcje płatnicze.",
@@ -570,6 +598,10 @@ export const dict = {
"enterprise.form.send": "Wyślij",
"enterprise.form.sending": "Wysyłanie...",
"enterprise.form.success": "Wiadomość wysłana, skontaktujemy się wkrótce.",
"enterprise.form.success.submitted": "Formularz został pomyślnie wysłany.",
"enterprise.form.error.allFieldsRequired": "Wszystkie pola są wymagane.",
"enterprise.form.error.invalidEmailFormat": "Nieprawidłowy format adresu e-mail.",
"enterprise.form.error.internalServer": "Wewnętrzny błąd serwera.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Czym jest OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -602,6 +634,7 @@ export const dict = {
"bench.list.table.agent": "Agent",
"bench.list.table.model": "Model",
"bench.list.table.score": "Wynik",
"bench.submission.error.allFieldsRequired": "Wszystkie pola są wymagane.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Nie znaleziono zadania",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Главная",
"nav.openMenu": "Открыть меню",
"nav.getStartedFree": "Начать бесплатно",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Скопировать логотип как SVG",
"nav.context.copyWordmark": "Скопировать название как SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Документация",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "светлый логотип opencode",
"notFound.logoDarkAlt": "темный логотип opencode",
"user.logout": "Выйти",
"auth.callback.error.codeMissing": "Код авторизации не найден.",
"workspace.select": "Выбрать рабочее пространство",
"workspace.createNew": "+ Создать рабочее пространство",
"workspace.modal.title": "Создать рабочее пространство",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "Сумма пополнения должна быть не менее ${{amount}}",
"error.reloadTriggerMin": "Порог баланса должен быть не менее ${{amount}}",
"app.meta.description": "OpenCode - AI-агент с открытым кодом для программирования.",
"home.title": "OpenCode | AI-агент с открытым кодом для программирования",
"temp.title": "opencode | AI-агент для программирования в терминале",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ", включая локальные модели",
"temp.screenshot.caption": "OpenCode TUI с темой tokyonight",
"temp.screenshot.alt": "OpenCode TUI с темой tokyonight",
"temp.logoLightAlt": "светлый логотип opencode",
"temp.logoDarkAlt": "темный логотип opencode",
"home.banner.badge": "Новое",
"home.banner.text": "Доступно десктопное приложение (бета)",
@@ -244,6 +253,24 @@ export const dict = {
"Все модели Zen размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
"zen.privacy.exceptionsLink": "следующими исключениями",
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
"zen.api.error.modelFormatNotSupported": "Модель {{model}} не поддерживается для формата {{format}}",
"zen.api.error.noProviderAvailable": "Нет доступных провайдеров",
"zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается",
"zen.api.error.missingApiKey": "Отсутствует API ключ.",
"zen.api.error.invalidApiKey": "Неверный API ключ.",
"zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Квота подписки превышена. Вы можете продолжить использовать бесплатные модели.",
"zen.api.error.noPaymentMethod": "Нет способа оплаты. Добавьте способ оплаты здесь: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Недостаточно средств. Управляйте оплатой здесь: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Ваше рабочее пространство достигло ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
"zen.api.error.modelDisabled": "Модель отключена",
"black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире",
"black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.",
"black.hero.title": "Доступ к лучшим моделям для кодинга в мире",
@@ -455,6 +482,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Пожалуйста, обновите способ оплаты и попробуйте снова.",
"workspace.reload.retrying": "Повторная попытка...",
"workspace.reload.retry": "Повторить",
"workspace.reload.error.paymentFailed": "Ошибка оплаты.",
"workspace.payments.title": "История платежей",
"workspace.payments.subtitle": "Недавние транзакции.",
@@ -574,6 +602,10 @@ export const dict = {
"enterprise.form.send": "Отправить",
"enterprise.form.sending": "Отправка...",
"enterprise.form.success": "Сообщение отправлено, мы скоро свяжемся с вами.",
"enterprise.form.success.submitted": "Форма успешно отправлена.",
"enterprise.form.error.allFieldsRequired": "Все поля обязательны.",
"enterprise.form.error.invalidEmailFormat": "Неверный формат email.",
"enterprise.form.error.internalServer": "Внутренняя ошибка сервера.",
"enterprise.faq.title": "FAQ",
"enterprise.faq.q1": "Что такое OpenCode Enterprise?",
"enterprise.faq.a1":
@@ -606,6 +638,7 @@ export const dict = {
"bench.list.table.agent": "Агент",
"bench.list.table.model": "Модель",
"bench.list.table.score": "Оценка",
"bench.submission.error.allFieldsRequired": "Все поля обязательны.",
"bench.detail.title": "Бенчмарк - {{task}}",
"bench.detail.notFound": "Задача не найдена",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "หน้าหลัก",
"nav.openMenu": "เปิดเมนู",
"nav.getStartedFree": "เริ่มต้นฟรี",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "คัดลอกโลโก้เป็น SVG",
"nav.context.copyWordmark": "คัดลอกตัวอักษรแบรนด์เป็น SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "เอกสาร",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "โลโก้ opencode แบบสว่าง",
"notFound.logoDarkAlt": "โลโก้ opencode แบบมืด",
"user.logout": "ออกจากระบบ",
"auth.callback.error.codeMissing": "ไม่พบ authorization code",
"workspace.select": "เลือก Workspace",
"workspace.createNew": "+ สร้าง Workspace ใหม่",
"workspace.modal.title": "สร้าง Workspace ใหม่",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "จำนวนเงินที่โหลดซ้ำต้องมีอย่างน้อย ${{amount}}",
"error.reloadTriggerMin": "ยอดคงเหลือที่กระตุ้นต้องมีอย่างน้อย ${{amount}}",
"app.meta.description": "OpenCode - เอเจนต์เขียนโค้ดแบบโอเพนซอร์ส",
"home.title": "OpenCode | เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส",
"temp.title": "OpenCode | เอเจนต์เขียนโค้ด AI ที่สร้างมาเพื่อเทอร์มินัล",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": "รวมถึงโมเดล Local",
"temp.screenshot.caption": "OpenCode TUI พร้อมธีม tokyonight",
"temp.screenshot.alt": "OpenCode TUI พร้อมธีม tokyonight",
"temp.logoLightAlt": "โลโก้ opencode แบบสว่าง",
"temp.logoDarkAlt": "โลโก้ opencode แบบมืด",
"home.banner.badge": "ใหม่",
"home.banner.text": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า",
@@ -239,6 +248,24 @@ export const dict = {
"โมเดล Zen ทั้งหมดโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
"zen.api.error.modelFormatNotSupported": "ไม่รองรับโมเดล {{model}} สำหรับรูปแบบ {{format}}",
"zen.api.error.noProviderAvailable": "ไม่มีผู้ให้บริการที่พร้อมใช้งาน",
"zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}",
"zen.api.error.missingApiKey": "ไม่มี API key",
"zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง",
"zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"โควต้าการสมัครสมาชิกเกินขีดจำกัด คุณสามารถดำเนินการต่อโดยใช้โมเดลฟรี",
"zen.api.error.noPaymentMethod": "ไม่มีวิธีการชำระเงิน เพิ่มวิธีการชำระเงินที่นี่: {{billingUrl}}",
"zen.api.error.insufficientBalance": "ยอดเงินคงเหลือไม่เพียงพอ จัดการการเรียกเก็บเงินของคุณที่นี่: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Workspace ของคุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
"black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
"black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black",
"black.hero.title": "เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
@@ -448,6 +475,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "โปรดอัปเดตวิธีการชำระเงินของคุณแล้วลองอีกครั้ง",
"workspace.reload.retrying": "กำลังลองอีกครั้ง...",
"workspace.reload.retry": "ลองอีกครั้ง",
"workspace.reload.error.paymentFailed": "การชำระเงินล้มเหลว",
"workspace.payments.title": "ประวัติการชำระเงิน",
"workspace.payments.subtitle": "รายการธุรกรรมการชำระเงินล่าสุด",
@@ -566,6 +594,10 @@ export const dict = {
"enterprise.form.send": "ส่ง",
"enterprise.form.sending": "กำลังส่ง...",
"enterprise.form.success": "ส่งข้อความแล้ว เราจะติดต่อกลับเร็วๆ นี้",
"enterprise.form.success.submitted": "ส่งแบบฟอร์มสำเร็จแล้ว",
"enterprise.form.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
"enterprise.form.error.invalidEmailFormat": "รูปแบบอีเมลไม่ถูกต้อง",
"enterprise.form.error.internalServer": "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
"enterprise.faq.title": "คำถามที่พบบ่อย",
"enterprise.faq.q1": "OpenCode Enterprise คืออะไร?",
"enterprise.faq.a1":
@@ -598,6 +630,7 @@ export const dict = {
"bench.list.table.agent": "เอเจนต์",
"bench.list.table.model": "โมเดล",
"bench.list.table.score": "คะแนน",
"bench.submission.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "ไม่พบงาน",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "Ana sayfa",
"nav.openMenu": "Menüyü aç",
"nav.getStartedFree": "Ücretsiz başla",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "Logoyu SVG olarak kopyala",
"nav.context.copyWordmark": "Wordmark'ı SVG olarak kopyala",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "Dokümantasyon",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode açık logo",
"notFound.logoDarkAlt": "opencode koyu logo",
"user.logout": ıkış",
"auth.callback.error.codeMissing": "Yetkilendirme kodu bulunamadı.",
"workspace.select": "Çalışma alanı seç",
"workspace.createNew": "+ Yeni çalışma alanı oluştur",
"workspace.modal.title": "Yeni çalışma alanı oluştur",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "Yükleme tutarı en az ${{amount}} olmalıdır",
"error.reloadTriggerMin": "Bakiye tetikleyicisi en az ${{amount}} olmalıdır",
"app.meta.description": "OpenCode - Açık kaynaklı kodlama ajanı.",
"home.title": "OpenCode | Açık kaynaklı yapay zeka kodlama ajanı",
"temp.title": "opencode | Terminal için geliştirilmiş yapay zeka kodlama ajanı",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": " üzerinden destekler",
"temp.screenshot.caption": "opencode TUI ve tokyonight teması",
"temp.screenshot.alt": "tokyonight temalı opencode TUI",
"temp.logoLightAlt": "opencode açık logo",
"temp.logoDarkAlt": "opencode koyu logo",
"home.banner.badge": "Yeni",
"home.banner.text": "Masaüstü uygulaması beta olarak kullanılabilir",
@@ -242,6 +251,24 @@ export const dict = {
"Tüm Zen modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
"zen.api.error.modelFormatNotSupported": "{{model}} modeli {{format}} formatı için desteklenmiyor",
"zen.api.error.noProviderAvailable": "Kullanılabilir sağlayıcı yok",
"zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor",
"zen.api.error.missingApiKey": "API anahtarı eksik.",
"zen.api.error.invalidApiKey": "Geçersiz API anahtarı.",
"zen.api.error.subscriptionQuotaExceeded": "Abonelik kotasııldı. {{retryIn}} içinde tekrar deneyin.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"Abonelik kotasııldı. Ücretsiz modelleri kullanmaya devam edebilirsiniz.",
"zen.api.error.noPaymentMethod": "Ödeme yöntemi bulunamadı. Buradan bir ödeme yöntemi ekleyin: {{billingUrl}}",
"zen.api.error.insufficientBalance": "Yetersiz bakiye. Faturalandırmanızı buradan yönetin: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"Çalışma alanınız aylık ${{amount}} harcama limitine ulaştı. Limitlerinizi buradan yönetin: {{billingUrl}}",
"zen.api.error.userMonthlyLimitReached":
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model devre dışı",
"black.meta.title": "OpenCode Black | Dünyanın en iyi kodlama modellerine erişin",
"black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.",
"black.hero.title": "Dünyanın en iyi kodlama modellerine erişin",
@@ -451,6 +478,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "Lütfen ödeme yönteminizi güncelleyin ve tekrar deneyin.",
"workspace.reload.retrying": "Yeniden deneniyor...",
"workspace.reload.retry": "Yeniden dene",
"workspace.reload.error.paymentFailed": "Ödeme başarısız.",
"workspace.payments.title": "Ödeme Geçmişi",
"workspace.payments.subtitle": "Son ödeme işlemleri.",
@@ -571,6 +599,10 @@ export const dict = {
"enterprise.form.send": "Gönder",
"enterprise.form.sending": "Gönderiliyor...",
"enterprise.form.success": "Mesaj gönderildi, yakında size dönüş yapacağız.",
"enterprise.form.success.submitted": "Form başarıyla gönderildi.",
"enterprise.form.error.allFieldsRequired": "Tüm alanlar gereklidir.",
"enterprise.form.error.invalidEmailFormat": "Geçersiz e-posta formatı.",
"enterprise.form.error.internalServer": "İç sunucu hatası.",
"enterprise.faq.title": "SSS",
"enterprise.faq.q1": "OpenCode Enterprise nedir?",
"enterprise.faq.a1":
@@ -603,6 +635,7 @@ export const dict = {
"bench.list.table.agent": "Ajan",
"bench.list.table.model": "Model",
"bench.list.table.score": "Puan",
"bench.submission.error.allFieldsRequired": "Tüm alanlar gereklidir.",
"bench.detail.title": "Benchmark - {{task}}",
"bench.detail.notFound": "Görev bulunamadı",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "首页",
"nav.openMenu": "打开菜单",
"nav.getStartedFree": "免费开始",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "复制 Logo (SVG)",
"nav.context.copyWordmark": "复制商标 (SVG)",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "文档",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode logo 亮色",
"notFound.logoDarkAlt": "opencode logo 暗色",
"user.logout": "退出登录",
"auth.callback.error.codeMissing": "未找到授权码。",
"workspace.select": "选择工作区",
"workspace.createNew": "+ 新建工作区",
"workspace.modal.title": "新建工作区",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "充值金额必须至少为 ${{amount}}",
"error.reloadTriggerMin": "余额触发阈值必须至少为 ${{amount}}",
"app.meta.description": "OpenCode - 开源编程代理。",
"home.title": "OpenCode | 开源 AI 编程代理",
"temp.title": "OpenCode | 专为终端打造的 AI 编程代理",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": ",包括本地模型",
"temp.screenshot.caption": "使用 Tokyonight 主题的 OpenCode TUI",
"temp.screenshot.alt": "使用 Tokyonight 主题的 OpenCode TUI",
"temp.logoLightAlt": "opencode logo 亮色",
"temp.logoDarkAlt": "opencode logo 暗色",
"home.banner.badge": "新",
"home.banner.text": "桌面应用 Beta 版现已推出",
@@ -229,6 +238,22 @@ export const dict = {
"zen.privacy.beforeExceptions": "所有 Zen 模型均托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
"zen.privacy.exceptionsLink": "以下例外情况除外",
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
"zen.api.error.modelFormatNotSupported": "格式 {{format}} 不支持模型 {{model}}",
"zen.api.error.noProviderAvailable": "没有可用的提供商",
"zen.api.error.providerNotSupported": "不支持提供商 {{provider}}",
"zen.api.error.missingApiKey": "缺少 API 密钥。",
"zen.api.error.invalidApiKey": "无效的 API 密钥。",
"zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。",
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出订阅配额。您可以继续使用免费模型。",
"zen.api.error.noPaymentMethod": "没有付款方式。请在此处添加付款方式:{{billingUrl}}",
"zen.api.error.insufficientBalance": "余额不足。请在此处管理您的计费:{{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已禁用",
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
"black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。",
"black.hero.title": "访问全球顶尖编程模型",
@@ -436,6 +461,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
"workspace.reload.retrying": "正在重试...",
"workspace.reload.retry": "重试",
"workspace.reload.error.paymentFailed": "支付失败。",
"workspace.payments.title": "支付历史",
"workspace.payments.subtitle": "近期支付交易。",
@@ -552,6 +578,10 @@ export const dict = {
"enterprise.form.send": "发送",
"enterprise.form.sending": "正在发送...",
"enterprise.form.success": "消息已发送,我们会尽快与您联系。",
"enterprise.form.success.submitted": "表单提交成功。",
"enterprise.form.error.allFieldsRequired": "所有字段均为必填项。",
"enterprise.form.error.invalidEmailFormat": "邮箱格式无效。",
"enterprise.form.error.internalServer": "内部服务器错误。",
"enterprise.faq.title": "常见问题",
"enterprise.faq.q1": "什么是 OpenCode 企业版?",
"enterprise.faq.a1":
@@ -584,6 +614,7 @@ export const dict = {
"bench.list.table.agent": "代理",
"bench.list.table.model": "模型",
"bench.list.table.score": "分数",
"bench.submission.error.allFieldsRequired": "所有字段均为必填项。",
"bench.detail.title": "基准测试 - {{task}}",
"bench.detail.notFound": "未找到任务",

View File

@@ -15,6 +15,7 @@ export const dict = {
"nav.home": "首頁",
"nav.openMenu": "開啟選單",
"nav.getStartedFree": "免費開始使用",
"nav.logoAlt": "OpenCode",
"nav.context.copyLogo": "複製標誌SVG",
"nav.context.copyWordmark": "複製字標SVG",
@@ -42,9 +43,13 @@ export const dict = {
"notFound.docs": "文件",
"notFound.github": "GitHub",
"notFound.discord": "Discord",
"notFound.logoLightAlt": "opencode 淺色標誌",
"notFound.logoDarkAlt": "opencode 深色標誌",
"user.logout": "登出",
"auth.callback.error.codeMissing": "找不到授權碼。",
"workspace.select": "選取工作區",
"workspace.createNew": "+ 建立新工作區",
"workspace.modal.title": "建立新工作區",
@@ -76,6 +81,8 @@ export const dict = {
"error.reloadAmountMin": "儲值金額必須至少為 ${{amount}}",
"error.reloadTriggerMin": "餘額觸發門檻必須至少為 ${{amount}}",
"app.meta.description": "OpenCode - 開源編碼代理。",
"home.title": "OpenCode | 開源 AI 編碼代理",
"temp.title": "OpenCode | 專為終端打造的 AI 編碼代理",
@@ -91,6 +98,8 @@ export const dict = {
"temp.feature.models.afterLink": "支援 75+ 家 LLM 供應商,包括本地模型",
"temp.screenshot.caption": "使用 tokyonight 主題的 OpenCode TUI",
"temp.screenshot.alt": "使用 tokyonight 主題的 OpenCode TUI",
"temp.logoLightAlt": "opencode 淺色標誌",
"temp.logoDarkAlt": "opencode 深色標誌",
"home.banner.badge": "新",
"home.banner.text": "桌面應用已推出 Beta",
@@ -229,6 +238,22 @@ export const dict = {
"zen.privacy.beforeExceptions": "所有 Zen 模型均在美國託管。供應商遵循零留存政策,不會將你的資料用於模型訓練,並且有",
"zen.privacy.exceptionsLink": "以下例外情況",
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
"zen.api.error.modelFormatNotSupported": "模型 {{model}} 不支援格式 {{format}}",
"zen.api.error.noProviderAvailable": "無可用的供應商",
"zen.api.error.providerNotSupported": "不支援供應商 {{provider}}",
"zen.api.error.missingApiKey": "缺少 API 金鑰。",
"zen.api.error.invalidApiKey": "無效的 API 金鑰。",
"zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。",
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出訂閱配額。你可以繼續使用免費模型。",
"zen.api.error.noPaymentMethod": "無付款方式。請在此處新增付款方式:{{billingUrl}}",
"zen.api.error.insufficientBalance": "餘額不足。請在此處管理你的帳務:{{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已停用",
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
"black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。",
"black.hero.title": "存取全球最佳編碼模型",
@@ -436,6 +461,7 @@ export const dict = {
"workspace.reload.updatePaymentMethod": "請更新你的付款方式並重試。",
"workspace.reload.retrying": "重試中...",
"workspace.reload.retry": "重試",
"workspace.reload.error.paymentFailed": "付款失敗。",
"workspace.payments.title": "付款紀錄",
"workspace.payments.subtitle": "最近的付款交易。",
@@ -551,6 +577,10 @@ export const dict = {
"enterprise.form.send": "傳送",
"enterprise.form.sending": "傳送中...",
"enterprise.form.success": "訊息已傳送,我們會盡快與你聯絡。",
"enterprise.form.success.submitted": "表單已成功送出。",
"enterprise.form.error.allFieldsRequired": "所有欄位均為必填。",
"enterprise.form.error.invalidEmailFormat": "無效的電子郵件格式。",
"enterprise.form.error.internalServer": "內部伺服器錯誤。",
"enterprise.faq.title": "常見問題",
"enterprise.faq.q1": "什麼是 OpenCode Enterprise",
"enterprise.faq.a1":
@@ -583,6 +613,7 @@ export const dict = {
"bench.list.table.agent": "代理",
"bench.list.table.model": "模型",
"bench.list.table.score": "分數",
"bench.submission.error.allFieldsRequired": "所有欄位均為必填。",
"bench.detail.title": "評測 - {{task}}",
"bench.detail.notFound": "找不到任務",

View File

@@ -48,6 +48,9 @@ const map = {
"Provider is required": "error.providerRequired",
"API key is required": "error.apiKeyRequired",
"Model is required": "error.modelRequired",
"workspace.reload.error.paymentFailed": "workspace.reload.error.paymentFailed",
"Payment failed": "workspace.reload.error.paymentFailed",
"Payment failed.": "workspace.reload.error.paymentFailed",
} as const satisfies Record<string, Key>
export function formErrorReloadAmountMin(amount: number) {

View File

@@ -16,8 +16,8 @@ export default function NotFound() {
<div data-component="content">
<section data-component="top">
<a href={language.route("/")} data-slot="logo-link">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
<img data-slot="logo light" src={logoLight} alt={i18n.t("notFound.logoLightAlt")} />
<img data-slot="logo dark" src={logoDark} alt={i18n.t("notFound.logoDarkAlt")} />
</a>
<h1 data-slot="title">{i18n.t("notFound.heading")}</h1>
</section>

View File

@@ -1,5 +1,7 @@
import type { APIEvent } from "@solidjs/start/server"
import { AWS } from "@opencode-ai/console-core/aws.js"
import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
interface EnterpriseFormData {
name: string
@@ -9,18 +11,19 @@ interface EnterpriseFormData {
}
export async function POST(event: APIEvent) {
const dict = i18n(localeFromRequest(event.request))
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 })
return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 })
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
return Response.json({ error: "Invalid email format" }, { status: 400 })
return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 })
}
// Create email content
@@ -39,9 +42,9 @@ ${body.email}`.trim()
replyTo: body.email,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
} catch (error) {
console.error("Error processing enterprise form:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
}
}

View File

@@ -2,15 +2,17 @@ import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
import { useAuthSession } from "~/context/auth"
import { i18n } from "~/i18n"
import { localeFromRequest, route } from "~/lib/language"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const locale = localeFromRequest(input.request)
const dict = i18n(locale)
try {
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
if (!code) throw new Error(dict["auth.callback.error.codeMissing"])
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) throw new Error(result.err.message)
const decoded = AuthClient.decode(result.tokens.access, {} as any)

View File

@@ -2,6 +2,8 @@ import type { APIEvent } from "@solidjs/start/server"
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
interface SubmissionBody {
model: string
@@ -10,10 +12,11 @@ interface SubmissionBody {
}
export async function POST(event: APIEvent) {
const dict = i18n(localeFromRequest(event.request))
const body = (await event.request.json()) as SubmissionBody
if (!body.model || !body.agent || !body.result) {
return Response.json({ error: "All fields are required" }, { status: 400 })
return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 })
}
await Database.use((tx) =>

View File

@@ -33,6 +33,7 @@ const brandAssets = "/opencode-brand-assets.zip"
export default function Brand() {
const i18n = useI18n()
const alt = i18n.t("brand.meta.description")
const downloadFile = async (url: string, filename: string) => {
try {
const response = await fetch(url)
@@ -88,7 +89,7 @@ export default function Brand() {
<div data-component="brand-grid">
<div>
<img src={previewLogoLight} alt="OpenCode brand guidelines" />
<img src={previewLogoLight} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
PNG
@@ -115,7 +116,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewLogoDark} alt="OpenCode brand guidelines" />
<img src={previewLogoDark} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
PNG
@@ -142,7 +143,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
<img src={previewLogoLightSquare} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
PNG
@@ -169,7 +170,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
<img src={previewLogoDarkSquare} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
PNG
@@ -196,7 +197,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<img src={previewWordmarkLight} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
PNG
@@ -223,7 +224,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
<img src={previewWordmarkDark} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
PNG
@@ -250,7 +251,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
<img src={previewWordmarkSimpleLight} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
PNG
@@ -277,7 +278,7 @@ export default function Brand() {
</div>
</div>
<div>
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
<img src={previewWordmarkSimpleDark} alt={alt} />
<div data-component="actions">
<button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
PNG

View File

@@ -19,7 +19,7 @@ const downloadNames: Record<string, string> = {
export async function GET({ params: { platform, channel } }: APIEvent) {
const assetName = assetNames[platform]
if (!assetName) return new Response("Not Found", { status: 404 })
if (!assetName) return new Response(null, { status: 404 })
const resp = await fetch(
`https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`,

View File

@@ -306,7 +306,7 @@ export async function POST(input: APIEvent) {
.update(BillingTable)
.set({
reload: false,
reloadError: errorMessage ?? "Payment failed.",
reloadError: errorMessage ?? "workspace.reload.error.paymentFailed",
timeReloadError: sql`now()`,
})
.where(eq(BillingTable.workspaceID, Actor.workspace())),

View File

@@ -47,8 +47,8 @@ export default function Home() {
<div data-component="content">
<section data-component="top">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
<img data-slot="logo light" src={logoLight} alt={i18n.t("temp.logoLightAlt")} />
<img data-slot="logo dark" src={logoDark} alt={i18n.t("temp.logoDarkAlt")} />
<h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
<div data-slot="login">
<a href="/auth">{i18n.t("temp.zen")}</a>

View File

@@ -12,6 +12,7 @@ import { queryBillingInfo } from "../../common"
import styles from "./lite-section.module.css"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { formError } from "~/lib/form-error"
const queryLiteSubscription = query(async (workspaceID: string) => {
"use server"
@@ -114,7 +115,7 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
const setLiteUseBalance = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
if (!workspaceID) return { error: formError.workspaceRequired }
const useBalance = form.get("useBalance")?.toString() === "true"
return json(

View File

@@ -202,7 +202,8 @@ export function ReloadSection() {
minute: "2-digit",
second: "2-digit",
})}
. {i18n.t("workspace.reload.reason")} {billingInfo()?.reloadError?.replace(/\.$/, "")}.{" "}
. {i18n.t("workspace.reload.reason")}{" "}
{localizeError(i18n.t, billingInfo()?.reloadError ?? undefined).replace(/\.$/, "")}.{" "}
{i18n.t("workspace.reload.updatePaymentMethod")}
</p>
<form action={reload} method="post" data-slot="create-form">

View File

@@ -35,6 +35,8 @@ import { createTrialLimiter } from "./trialLimiter"
import { createStickyTracker } from "./stickyProviderTracker"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { Resource } from "@opencode-ai/console-resource"
import { i18n, type Key } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
@@ -43,6 +45,15 @@ type RetryOptions = {
}
type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance"
function resolve(text: string, params?: Record<string, string | number>) {
if (!params) return text
return text.replace(/\{\{(\w+)\}\}/g, (raw, key) => {
const value = params[key]
if (value === undefined || value === null) return raw
return String(value)
})
}
export async function handler(
input: APIEvent,
opts: {
@@ -60,6 +71,8 @@ export async function handler(
const MAX_FAILOVER_RETRIES = 3
const MAX_429_RETRIES = 3
const dict = i18n(localeFromRequest(input.request))
const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params)
const ADMIN_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
@@ -86,7 +99,7 @@ export async function handler(
const dataDumper = createDataDumper(sessionId, requestId, projectId)
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers)
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
const stickyProvider = await stickyTracker?.get()
@@ -359,14 +372,20 @@ export async function handler(
}
function validateModel(zenData: ZenData, reqModel: string) {
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
const modelId = reqModel as keyof typeof zenData.models
const modelData = Array.isArray(zenData.models[modelId])
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
: zenData.models[modelId]
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
if (!modelData)
throw new ModelError(
t("zen.api.error.modelFormatNotSupported", {
model: reqModel,
format: opts.format,
}),
)
logger.metric({ model: modelId })
@@ -418,8 +437,9 @@ export async function handler(
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
})()
if (!modelProvider) throw new ModelError("No provider available")
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
if (!(modelProvider.id in zenData.providers))
throw new ModelError(t("zen.api.error.providerNotSupported", { provider: modelProvider.id }))
return {
...modelProvider,
@@ -439,7 +459,7 @@ export async function handler(
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
throw new AuthError("Missing API key.")
throw new AuthError(t("zen.api.error.missingApiKey"))
}
const data = await Database.use((tx) =>
@@ -520,13 +540,13 @@ export async function handler(
.then((rows) => rows[0]),
)
if (!data) throw new AuthError("Invalid API key.")
if (!data) throw new AuthError(t("zen.api.error.invalidApiKey"))
if (
modelInfo.id.startsWith("alpha-") &&
Resource.App.stage === "production" &&
!ADMIN_WORKSPACES.includes(data.workspaceID)
)
throw new AuthError(`Model ${modelInfo.id} not supported`)
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
logger.metric({
api_key: data.apiKey,
@@ -590,7 +610,9 @@ export async function handler(
})
if (result.status === "rate-limited")
throw new SubscriptionUsageLimitError(
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
t("zen.api.error.subscriptionQuotaExceeded", {
retryIn: formatRetryTime(result.resetInSec),
}),
result.resetInSec,
)
}
@@ -606,7 +628,9 @@ export async function handler(
})
if (result.status === "rate-limited")
throw new SubscriptionUsageLimitError(
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
t("zen.api.error.subscriptionQuotaExceeded", {
retryIn: formatRetryTime(result.resetInSec),
}),
result.resetInSec,
)
}
@@ -632,7 +656,7 @@ export async function handler(
})
if (result.status === "rate-limited")
throw new SubscriptionUsageLimitError(
`Subscription quota exceeded. You can continue using free models.`,
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
result.resetInSec,
)
}
@@ -647,7 +671,7 @@ export async function handler(
})
if (result.status === "rate-limited")
throw new SubscriptionUsageLimitError(
`Subscription quota exceeded. You can continue using free models.`,
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
result.resetInSec,
)
}
@@ -662,7 +686,7 @@ export async function handler(
})
if (result.status === "rate-limited")
throw new SubscriptionUsageLimitError(
`Subscription quota exceeded. You can continue using free models.`,
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
result.resetInSec,
)
}
@@ -675,14 +699,10 @@ export async function handler(
// Validate pay as you go billing
const billing = authInfo.billing
if (!billing.paymentMethodID)
throw new CreditsError(
`No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
if (billing.balance <= 0)
throw new CreditsError(
`Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
const now = new Date()
const currentYear = now.getUTCFullYear()
@@ -696,7 +716,10 @@ export async function handler(
currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
)
throw new MonthlyLimitError(
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
t("zen.api.error.workspaceMonthlyLimitReached", {
amount: billing.monthlyLimit,
billingUrl,
}),
)
if (
@@ -708,7 +731,10 @@ export async function handler(
currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
)
throw new UserLimitError(
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
t("zen.api.error.userMonthlyLimitReached", {
amount: authInfo.user.monthlyLimit,
membersUrl,
}),
)
return "balance"
@@ -716,7 +742,7 @@ export async function handler(
function validateModelSettings(authInfo: AuthInfo) {
if (!authInfo) return
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
if (authInfo.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled"))
}
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {

View File

@@ -3,11 +3,14 @@ import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { FreeUsageLimitError } from "./error"
import { logger } from "./logger"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) {
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) {
if (!limit) return
const dict = i18n(localeFromRequest(request))
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
const ip = !rawIp.length ? "unknown" : rawIp
const now = Date.now()
@@ -36,7 +39,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
logger.debug(`rate limit total: ${total}`)
if (total >= limitValue)
throw new FreeUsageLimitError(
`Rate limit exceeded. Please try again later.`,
dict["zen.api.error.rateLimitExceeded"],
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
)
},

View File

@@ -0,0 +1,7 @@
CREATE TABLE `workspace` (
`id` text PRIMARY KEY,
`branch` text,
`project_id` text NOT NULL,
`config` text NOT NULL,
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);

View File

@@ -0,0 +1,959 @@
{
"version": "7",
"dialect": "sqlite",
"id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40",
"prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"],
"ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{
"name": "control_account",
"entityType": "tables"
},
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "config",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "access_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "refresh_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "token_expiry",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "active",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "commands",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_workspace_project_id_project_id_fk",
"entityType": "fks",
"table": "workspace"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}

View File

@@ -2,6 +2,9 @@ import { Server } from "../../server/server"
import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "../../flag/flag"
import { Workspace } from "../../control-plane/workspace"
import { Project } from "../../project/project"
import { Installation } from "../../installation"
export const ServeCommand = cmd({
command: "serve",
@@ -14,7 +17,15 @@ export const ServeCommand = cmd({
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
let workspaceSync: Array<ReturnType<typeof Workspace.startSyncing>> = []
// Only available in development right now
if (Installation.isLocal()) {
workspaceSync = Project.list().map((project) => Workspace.startSyncing(project))
}
await new Promise(() => {})
await server.stop()
await Promise.all(workspaceSync.map((item) => item.stop()))
},
})

View File

@@ -1,59 +1,16 @@
import { cmd } from "./cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Installation } from "../../installation"
import { WorkspaceServer } from "../../control-plane/workspace-server/server"
export const WorkspaceServeCommand = cmd({
command: "workspace-serve",
builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a remote workspace websocket server",
describe: "starts a remote workspace event server",
handler: async (args) => {
const opts = await resolveNetworkOptions(args)
const server = Bun.serve<{ id: string }>({
hostname: opts.hostname,
port: opts.port,
fetch(req, server) {
const url = new URL(req.url)
if (url.pathname === "/ws") {
const id = Bun.randomUUIDv7()
if (server.upgrade(req, { data: { id } })) return
return new Response("Upgrade failed", { status: 400 })
}
if (url.pathname === "/health") {
return new Response("ok", {
status: 200,
headers: {
"content-type": "text/plain; charset=utf-8",
},
})
}
return new Response(
JSON.stringify({
service: "workspace-server",
ws: `ws://${server.hostname}:${server.port}/ws`,
}),
{
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
},
},
)
},
websocket: {
open(ws) {
ws.send(JSON.stringify({ type: "ready", id: ws.data.id }))
},
message(ws, msg) {
const text = typeof msg === "string" ? msg : msg.toString()
ws.send(JSON.stringify({ type: "message", id: ws.data.id, text }))
},
close() {},
},
})
console.log(`workspace websocket server listening on ws://${server.hostname}:${server.port}/ws`)
const server = WorkspaceServer.Listen(opts)
console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
await new Promise(() => {})
await server.stop()
},
})

View File

@@ -0,0 +1,10 @@
import { WorktreeAdaptor } from "./worktree"
import type { Config } from "../config"
import type { Adaptor } from "./types"
export function getAdaptor(config: Config): Adaptor {
switch (config.type) {
case "worktree":
return WorktreeAdaptor
}
}

View File

@@ -0,0 +1,7 @@
import type { Config } from "../config"
export type Adaptor<T extends Config = Config> = {
create(from: T, branch?: string | null): Promise<{ config: T; init: () => Promise<void> }>
remove(from: T): Promise<void>
request(from: T, method: string, url: string, data?: BodyInit, signal?: AbortSignal): Promise<Response | undefined>
}

View File

@@ -0,0 +1,26 @@
import { Worktree } from "@/worktree"
import type { Config } from "../config"
import type { Adaptor } from "./types"
type WorktreeConfig = Extract<Config, { type: "worktree" }>
export const WorktreeAdaptor: Adaptor<WorktreeConfig> = {
async create(_from: WorktreeConfig, _branch: string) {
const next = await Worktree.create(undefined)
return {
config: {
type: "worktree",
directory: next.directory,
},
// Hack for now: `Worktree.create` puts all its async code in a
// `setTimeout` so it doesn't use this, but we should change that
init: async () => {},
}
},
async remove(config: WorktreeConfig) {
await Worktree.remove({ directory: config.directory })
},
async request(_from: WorktreeConfig, _method: string, _url: string, _data?: BodyInit, _signal?: AbortSignal) {
throw new Error("worktree does not support request")
},
}

View File

@@ -0,0 +1,10 @@
import z from "zod"
export const Config = z.discriminatedUnion("type", [
z.object({
directory: z.string(),
type: z.literal("worktree"),
}),
])
export type Config = z.infer<typeof Config>

View File

@@ -0,0 +1,46 @@
import { Instance } from "@/project/instance"
import type { MiddlewareHandler } from "hono"
import { Installation } from "../installation"
import { getAdaptor } from "./adaptors"
import { Workspace } from "./workspace"
// This middleware forwards all non-GET requests if the workspace is a
// remote. The remote workspace needs to handle session mutations
async function proxySessionRequest(req: Request) {
if (req.method === "GET") return
if (!Instance.directory.startsWith("wrk_")) return
const workspace = await Workspace.get(Instance.directory)
if (!workspace) {
return new Response(`Workspace not found: ${Instance.directory}`, {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
})
}
if (workspace.config.type === "worktree") return
const url = new URL(req.url)
const body = req.method === "HEAD" ? undefined : await req.arrayBuffer()
return getAdaptor(workspace.config).request(
workspace.config,
req.method,
`${url.pathname}${url.search}`,
body,
req.signal,
)
}
export const SessionProxyMiddleware: MiddlewareHandler = async (c, next) => {
// Only available in development for now
if (!Installation.isLocal()) {
return next()
}
const response = await proxySessionRequest(c.req.raw)
if (response) {
return response
}
return next()
}

View File

@@ -0,0 +1,66 @@
export async function parseSSE(
body: ReadableStream<Uint8Array>,
signal: AbortSignal,
onEvent: (event: unknown) => void,
) {
const reader = body.getReader()
const decoder = new TextDecoder()
let buf = ""
let last = ""
let retry = 1000
const abort = () => {
void reader.cancel().catch(() => undefined)
}
signal.addEventListener("abort", abort)
try {
while (!signal.aborted) {
const chunk = await reader.read().catch(() => ({ done: true, value: undefined as Uint8Array | undefined }))
if (chunk.done) break
buf += decoder.decode(chunk.value, { stream: true })
buf = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const chunks = buf.split("\n\n")
buf = chunks.pop() ?? ""
chunks.forEach((chunk) => {
const data: string[] = []
chunk.split("\n").forEach((line) => {
if (line.startsWith("data:")) {
data.push(line.replace(/^data:\s*/, ""))
return
}
if (line.startsWith("id:")) {
last = line.replace(/^id:\s*/, "")
return
}
if (line.startsWith("retry:")) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
if (!Number.isNaN(parsed)) retry = parsed
}
})
if (!data.length) return
const raw = data.join("\n")
try {
onEvent(JSON.parse(raw))
} catch {
onEvent({
type: "sse.message",
properties: {
data: raw,
id: last || undefined,
retry,
},
})
}
})
}
} finally {
signal.removeEventListener("abort", abort)
reader.releaseLock()
}
}

View File

@@ -0,0 +1,33 @@
import { GlobalBus } from "../../bus/global"
import { Hono } from "hono"
import { streamSSE } from "hono/streaming"
export function WorkspaceServerRoutes() {
return new Hono().get("/event", async (c) => {
c.header("X-Accel-Buffering", "no")
c.header("X-Content-Type-Options", "nosniff")
return streamSSE(c, async (stream) => {
const send = async (event: unknown) => {
await stream.writeSSE({
data: JSON.stringify(event),
})
}
const handler = async (event: { directory?: string; payload: unknown }) => {
await send(event.payload)
}
GlobalBus.on("event", handler)
await send({ type: "server.connected", properties: {} })
const heartbeat = setInterval(() => {
void send({ type: "server.heartbeat", properties: {} })
}, 10_000)
await new Promise<void>((resolve) => {
stream.onAbort(() => {
clearInterval(heartbeat)
GlobalBus.off("event", handler)
resolve()
})
})
})
})
}

View File

@@ -0,0 +1,24 @@
import { Hono } from "hono"
import { SessionRoutes } from "../../server/routes/session"
import { WorkspaceServerRoutes } from "./routes"
export namespace WorkspaceServer {
export function App() {
const session = new Hono()
.use("*", async (c, next) => {
if (c.req.method === "GET") return c.notFound()
await next()
})
.route("/", SessionRoutes())
return new Hono().route("/session", session).route("/", WorkspaceServerRoutes())
}
export function Listen(opts: { hostname: string; port: number }) {
return Bun.serve({
hostname: opts.hostname,
port: opts.port,
fetch: App().fetch,
})
}
}

View File

@@ -0,0 +1,12 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "@/project/project.sql"
import type { Config } from "./config"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(),
branch: text(),
project_id: text()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
config: text({ mode: "json" }).notNull().$type<Config>(),
})

View File

@@ -0,0 +1,160 @@
import z from "zod"
import { Identifier } from "@/id/id"
import { fn } from "@/util/fn"
import { Database, eq } from "@/storage/db"
import { Project } from "@/project/project"
import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { Log } from "@/util/log"
import { WorkspaceTable } from "./workspace.sql"
import { Config } from "./config"
import { getAdaptor } from "./adaptors"
import { parseSSE } from "./sse"
export namespace Workspace {
export const Event = {
Ready: BusEvent.define(
"workspace.ready",
z.object({
name: z.string(),
}),
),
Failed: BusEvent.define(
"workspace.failed",
z.object({
message: z.string(),
}),
),
}
export const Info = z
.object({
id: Identifier.schema("workspace"),
branch: z.string().nullable(),
projectID: z.string(),
config: Config,
})
.meta({
ref: "Workspace",
})
export type Info = z.infer<typeof Info>
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return {
id: row.id,
branch: row.branch,
projectID: row.project_id,
config: row.config,
}
}
export const create = fn(
z.object({
id: Identifier.schema("workspace").optional(),
projectID: Info.shape.projectID,
branch: Info.shape.branch,
config: Info.shape.config,
}),
async (input) => {
const id = Identifier.ascending("workspace", input.id)
const { config, init } = await getAdaptor(input.config).create(input.config, input.branch)
const info: Info = {
id,
projectID: input.projectID,
branch: input.branch,
config,
}
setTimeout(async () => {
await init()
Database.use((db) => {
db.insert(WorkspaceTable)
.values({
id: info.id,
branch: info.branch,
project_id: info.projectID,
config: info.config,
})
.run()
})
GlobalBus.emit("event", {
directory: id,
payload: {
type: Event.Ready.type,
properties: {},
},
})
}, 0)
return info
},
)
export function list(project: Project.Info) {
const rows = Database.use((db) =>
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
)
return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
}
export const get = fn(Identifier.schema("workspace"), async (id) => {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (!row) return
return fromRow(row)
})
export const remove = fn(Identifier.schema("workspace"), async (id) => {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (row) {
const info = fromRow(row)
await getAdaptor(info.config).remove(info.config)
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info
}
})
const log = Log.create({ service: "workspace-sync" })
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
while (!stop.aborted) {
const res = await getAdaptor(space.config)
.request(space.config, "GET", "/event", undefined, stop)
.catch(() => undefined)
if (!res || !res.ok || !res.body) {
await Bun.sleep(1000)
continue
}
await parseSSE(res.body, stop, (event) => {
GlobalBus.emit("event", {
directory: space.id,
payload: event,
})
})
// Wait 250ms and retry if SSE connection fails
await Bun.sleep(250)
}
}
export function startSyncing(project: Project.Info) {
const stop = new AbortController()
const spaces = list(project).filter((space) => space.config.type !== "worktree")
spaces.forEach((space) => {
void workspaceEventLoop(space, stop.signal).catch((error) => {
log.warn("workspace sync listener failed", {
workspaceID: space.id,
error,
})
})
})
return {
async stop() {
stop.abort()
},
}
}
}

View File

@@ -11,6 +11,7 @@ export namespace Identifier {
part: "prt",
pty: "pty",
tool: "tool",
workspace: "wrk",
} as const
export function schema(prefix: keyof typeof prefixes) {

View File

@@ -10,6 +10,7 @@ import { Session } from "../../session"
import { zodToJsonSchema } from "zod-to-json-schema"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
import { WorkspaceRoutes } from "./workspace"
export const ExperimentalRoutes = lazy(() =>
new Hono()
@@ -112,6 +113,7 @@ export const ExperimentalRoutes = lazy(() =>
return c.json(worktree)
},
)
.route("/workspace", WorkspaceRoutes())
.get(
"/worktree",
describeRoute({

View File

@@ -16,11 +16,13 @@ import { Log } from "../../util/log"
import { PermissionNext } from "@/permission/next"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
import { SessionProxyMiddleware } from "../../control-plane/session-proxy-middleware"
const log = Log.create({ service: "server" })
export const SessionRoutes = lazy(() =>
new Hono()
.use(SessionProxyMiddleware)
.get(
"/",
describeRoute({

View File

@@ -0,0 +1,104 @@
import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { Workspace } from "../../control-plane/workspace"
import { Instance } from "../../project/instance"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
export const WorkspaceRoutes = lazy(() =>
new Hono()
.post(
"/:id",
describeRoute({
summary: "Create workspace",
description: "Create a workspace for the current project.",
operationId: "experimental.workspace.create",
responses: {
200: {
description: "Workspace created",
content: {
"application/json": {
schema: resolver(Workspace.Info),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
id: Workspace.Info.shape.id,
}),
),
validator(
"json",
z.object({
branch: Workspace.Info.shape.branch,
config: Workspace.Info.shape.config,
}),
),
async (c) => {
const { id } = c.req.valid("param")
const body = c.req.valid("json")
const workspace = await Workspace.create({
id,
projectID: Instance.project.id,
branch: body.branch,
config: body.config,
})
return c.json(workspace)
},
)
.get(
"/",
describeRoute({
summary: "List workspaces",
description: "List all workspaces.",
operationId: "experimental.workspace.list",
responses: {
200: {
description: "Workspaces",
content: {
"application/json": {
schema: resolver(z.array(Workspace.Info)),
},
},
},
},
}),
async (c) => {
return c.json(Workspace.list(Instance.project))
},
)
.delete(
"/:id",
describeRoute({
summary: "Remove workspace",
description: "Remove an existing workspace.",
operationId: "experimental.workspace.remove",
responses: {
200: {
description: "Workspace removed",
content: {
"application/json": {
schema: resolver(Workspace.Info.optional()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
id: Workspace.Info.shape.id,
}),
),
async (c) => {
const { id } = c.req.valid("param")
return c.json(await Workspace.remove(id))
},
),
)

View File

@@ -2,3 +2,4 @@ export { ControlAccountTable } from "../control/control.sql"
export { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
export { SessionShareTable } from "../share/share.sql"
export { ProjectTable } from "../project/project.sql"
export { WorkspaceTable } from "../control-plane/workspace.sql"

View File

@@ -0,0 +1,147 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import { Identifier } from "../../src/id/id"
import { Hono } from "hono"
import { tmpdir } from "../fixture/fixture"
import { Project } from "../../src/project/project"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { Instance } from "../../src/project/instance"
import { Database } from "../../src/storage/db"
import { resetDatabase } from "../fixture/db"
afterEach(async () => {
mock.restore()
await resetDatabase()
})
type State = {
workspace?: "first" | "second"
calls: Array<{ method: string; url: string; body?: string }>
}
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
async function setup(state: State) {
mock.module("../../src/control-plane/adaptors", () => ({
getAdaptor: () => ({
request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
const body = data ? await new Response(data).text() : undefined
state.calls.push({ method, url, body })
return new Response("proxied", { status: 202 })
},
}),
}))
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
const id1 = Identifier.descending("workspace")
const id2 = Identifier.descending("workspace")
Database.use((db) =>
db
.insert(WorkspaceTable)
.values([
{
id: id1,
branch: "main",
project_id: project.id,
config: remote,
},
{
id: id2,
branch: "main",
project_id: project.id,
config: { type: "worktree", directory: tmp.path },
},
])
.run(),
)
const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
const app = new Hono().use(SessionProxyMiddleware)
return {
id1,
id2,
app,
async request(input: RequestInfo | URL, init?: RequestInit) {
return Instance.provide({
directory: state.workspace === "first" ? id1 : id2,
fn: async () => app.request(input, init),
})
},
}
}
describe("control-plane/session-proxy-middleware", () => {
test("forwards non-GET session requests for remote workspaces", async () => {
const state: State = {
workspace: "first",
calls: [],
}
const ctx = await setup(state)
ctx.app.post("/session/foo", (c) => c.text("local", 200))
const response = await ctx.request("http://workspace.test/session/foo?x=1", {
method: "POST",
body: JSON.stringify({ hello: "world" }),
headers: {
"content-type": "application/json",
},
})
expect(response.status).toBe(202)
expect(await response.text()).toBe("proxied")
expect(state.calls).toEqual([
{
method: "POST",
url: "/session/foo?x=1",
body: '{"hello":"world"}',
},
])
})
test("does not forward GET requests", async () => {
const state: State = {
workspace: "first",
calls: [],
}
const ctx = await setup(state)
ctx.app.get("/session/foo", (c) => c.text("local", 200))
const response = await ctx.request("http://workspace.test/session/foo?x=1")
expect(response.status).toBe(200)
expect(await response.text()).toBe("local")
expect(state.calls).toEqual([])
})
test("does not forward GET or POST requests for worktree workspaces", async () => {
const state: State = {
workspace: "second",
calls: [],
}
const ctx = await setup(state)
ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
method: "POST",
body: JSON.stringify({ hello: "world" }),
headers: {
"content-type": "application/json",
},
})
expect(getResponse.status).toBe(200)
expect(await getResponse.text()).toBe("local-get")
expect(postResponse.status).toBe(200)
expect(await postResponse.text()).toBe("local-post")
expect(state.calls).toEqual([])
})
})

View File

@@ -0,0 +1,56 @@
import { afterEach, describe, expect, test } from "bun:test"
import { parseSSE } from "../../src/control-plane/sse"
import { resetDatabase } from "../fixture/db"
afterEach(async () => {
await resetDatabase()
})
function stream(chunks: string[]) {
return new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder()
chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk)))
controller.close()
},
})
}
describe("control-plane/sse", () => {
test("parses JSON events with CRLF and multiline data blocks", async () => {
const events: unknown[] = []
const stop = new AbortController()
await parseSSE(
stream([
'data: {"type":"one","properties":{"ok":true}}\r\n\r\n',
'data: {"type":"two",\r\ndata: "properties":{"n":2}}\r\n\r\n',
]),
stop.signal,
(event) => events.push(event),
)
expect(events).toEqual([
{ type: "one", properties: { ok: true } },
{ type: "two", properties: { n: 2 } },
])
})
test("falls back to sse.message for non-json payload", async () => {
const events: unknown[] = []
const stop = new AbortController()
await parseSSE(stream(["id: abc\nretry: 1500\ndata: hello world\n\n"]), stop.signal, (event) => events.push(event))
expect(events).toEqual([
{
type: "sse.message",
properties: {
data: "hello world",
id: "abc",
retry: 1500,
},
},
])
})
})

View File

@@ -0,0 +1,65 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Log } from "../../src/util/log"
import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
import { parseSSE } from "../../src/control-plane/sse"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
afterEach(async () => {
await resetDatabase()
})
Log.init({ print: false })
describe("control-plane/workspace-server SSE", () => {
test("streams GlobalBus events and parseSSE reads them", async () => {
const app = WorkspaceServer.App()
const stop = new AbortController()
const seen: unknown[] = []
try {
const response = await app.request("/event", {
signal: stop.signal,
})
expect(response.status).toBe(200)
expect(response.body).toBeDefined()
const done = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("timed out waiting for workspace.test event"))
}, 3000)
void parseSSE(response.body!, stop.signal, (event) => {
seen.push(event)
const next = event as { type?: string }
if (next.type === "server.connected") {
GlobalBus.emit("event", {
payload: {
type: "workspace.test",
properties: { ok: true },
},
})
return
}
if (next.type !== "workspace.test") return
clearTimeout(timeout)
resolve()
}).catch((error) => {
clearTimeout(timeout)
reject(error)
})
})
await done
expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
expect(seen).toContainEqual({
type: "workspace.test",
properties: { ok: true },
})
} finally {
stop.abort()
}
})
})

View File

@@ -0,0 +1,97 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import { Identifier } from "../../src/id/id"
import { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture"
import { Project } from "../../src/project/project"
import { Database } from "../../src/storage/db"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
afterEach(async () => {
mock.restore()
await resetDatabase()
})
Log.init({ print: false })
const seen: string[] = []
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
mock.module("../../src/control-plane/adaptors", () => ({
getAdaptor: (config: { type: string }) => {
seen.push(config.type)
return {
async create() {
throw new Error("not used")
},
async remove() {},
async request() {
const body = new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder()
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
controller.close()
},
})
return new Response(body, {
status: 200,
headers: {
"content-type": "text/event-stream",
},
})
},
}
},
}))
describe("control-plane/workspace.startSyncing", () => {
test("syncs only remote workspaces and emits remote SSE events", async () => {
const { Workspace } = await import("../../src/control-plane/workspace")
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
const id1 = Identifier.descending("workspace")
const id2 = Identifier.descending("workspace")
Database.use((db) =>
db
.insert(WorkspaceTable)
.values([
{
id: id1,
branch: "main",
project_id: project.id,
config: remote,
},
{
id: id2,
branch: "main",
project_id: project.id,
config: { type: "worktree", directory: tmp.path },
},
])
.run(),
)
const done = new Promise<void>((resolve) => {
const listener = (event: { directory?: string; payload: { type: string } }) => {
if (event.directory !== id1) return
if (event.payload.type !== "remote.ready") return
GlobalBus.off("event", listener)
resolve()
}
GlobalBus.on("event", listener)
})
const sync = Workspace.startSyncing(project)
await Promise.race([
done,
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out waiting for sync event")), 2000)),
])
await sync.stop()
expect(seen).toContain("testing")
expect(seen).not.toContain("worktree")
})
})

View File

@@ -0,0 +1,11 @@
import { rm } from "fs/promises"
import { Instance } from "../../src/project/instance"
import { Database } from "../../src/storage/db"
export async function resetDatabase() {
await Instance.disposeAll().catch(() => undefined)
Database.close()
await rm(Database.Path, { force: true }).catch(() => undefined)
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined)
}

View File

@@ -26,6 +26,11 @@ import type {
EventTuiToastShow,
ExperimentalResourceListResponses,
ExperimentalSessionListResponses,
ExperimentalWorkspaceCreateErrors,
ExperimentalWorkspaceCreateResponses,
ExperimentalWorkspaceListResponses,
ExperimentalWorkspaceRemoveErrors,
ExperimentalWorkspaceRemoveResponses,
FileListResponses,
FilePartInput,
FilePartSource,
@@ -901,6 +906,107 @@ export class Worktree extends HeyApiClient {
}
}
export class Workspace extends HeyApiClient {
/**
* Remove workspace
*
* Remove an existing workspace.
*/
public remove<ThrowOnError extends boolean = false>(
parameters: {
id: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "id" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).delete<
ExperimentalWorkspaceRemoveResponses,
ExperimentalWorkspaceRemoveErrors,
ThrowOnError
>({
url: "/experimental/workspace/{id}",
...options,
...params,
})
}
/**
* Create workspace
*
* Create a workspace for the current project.
*/
public create<ThrowOnError extends boolean = false>(
parameters: {
id: string
directory?: string
branch?: string | null
config?: {
directory: string
type: "worktree"
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "id" },
{ in: "query", key: "directory" },
{ in: "body", key: "branch" },
{ in: "body", key: "config" },
],
},
],
)
return (options?.client ?? this.client).post<
ExperimentalWorkspaceCreateResponses,
ExperimentalWorkspaceCreateErrors,
ThrowOnError
>({
url: "/experimental/workspace/{id}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* List workspaces
*
* List all workspaces.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
return (options?.client ?? this.client).get<ExperimentalWorkspaceListResponses, unknown, ThrowOnError>({
url: "/experimental/workspace",
...options,
...params,
})
}
}
export class Session extends HeyApiClient {
/**
* List sessions
@@ -965,6 +1071,11 @@ export class Resource extends HeyApiClient {
}
export class Experimental extends HeyApiClient {
private _workspace?: Workspace
get workspace(): Workspace {
return (this._workspace ??= new Workspace({ client: this.client }))
}
private _session?: Session
get session(): Session {
return (this._session ??= new Session({ client: this.client }))

View File

@@ -887,6 +887,35 @@ export type EventVcsBranchUpdated = {
}
}
export type EventWorktreeReady = {
type: "worktree.ready"
properties: {
name: string
branch: string
}
}
export type EventWorktreeFailed = {
type: "worktree.failed"
properties: {
message: string
}
}
export type EventWorkspaceReady = {
type: "workspace.ready"
properties: {
name: string
}
}
export type EventWorkspaceFailed = {
type: "workspace.failed"
properties: {
message: string
}
}
export type Pty = {
id: string
title: string
@@ -926,21 +955,6 @@ export type EventPtyDeleted = {
}
}
export type EventWorktreeReady = {
type: "worktree.ready"
properties: {
name: string
branch: string
}
}
export type EventWorktreeFailed = {
type: "worktree.failed"
properties: {
message: string
}
}
export type Event =
| EventInstallationUpdated
| EventInstallationUpdateAvailable
@@ -979,12 +993,14 @@ export type Event =
| EventSessionDiff
| EventSessionError
| EventVcsBranchUpdated
| EventWorktreeReady
| EventWorktreeFailed
| EventWorkspaceReady
| EventWorkspaceFailed
| EventPtyCreated
| EventPtyUpdated
| EventPtyExited
| EventPtyDeleted
| EventWorktreeReady
| EventWorktreeFailed
export type GlobalEvent = {
directory: string
@@ -1627,6 +1643,16 @@ export type WorktreeCreateInput = {
startCommand?: string
}
export type Workspace = {
id: string
branch: string | null
projectID: string
config: {
directory: string
type: "worktree"
}
}
export type WorktreeRemoveInput = {
directory: string
}
@@ -2473,6 +2499,93 @@ export type WorktreeCreateResponses = {
export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
export type ExperimentalWorkspaceRemoveData = {
body?: never
path: {
id: string
}
query?: {
directory?: string
}
url: "/experimental/workspace/{id}"
}
export type ExperimentalWorkspaceRemoveErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type ExperimentalWorkspaceRemoveError =
ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors]
export type ExperimentalWorkspaceRemoveResponses = {
/**
* Workspace removed
*/
200: Workspace
}
export type ExperimentalWorkspaceRemoveResponse =
ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
export type ExperimentalWorkspaceCreateData = {
body?: {
branch: string | null
config: {
directory: string
type: "worktree"
}
}
path: {
id: string
}
query?: {
directory?: string
}
url: "/experimental/workspace/{id}"
}
export type ExperimentalWorkspaceCreateErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type ExperimentalWorkspaceCreateError =
ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
export type ExperimentalWorkspaceCreateResponses = {
/**
* Workspace created
*/
200: Workspace
}
export type ExperimentalWorkspaceCreateResponse =
ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
export type ExperimentalWorkspaceListData = {
body?: never
path?: never
query?: {
directory?: string
}
url: "/experimental/workspace"
}
export type ExperimentalWorkspaceListResponses = {
/**
* Workspaces
*/
200: Array<Workspace>
}
export type ExperimentalWorkspaceListResponse =
ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
export type WorktreeResetData = {
body?: WorktreeResetInput
path?: never

View File

@@ -1149,6 +1149,186 @@
]
}
},
"/experimental/workspace/{id}": {
"post": {
"operationId": "experimental.workspace.create",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "id",
"schema": {
"type": "string",
"pattern": "^wrk.*"
},
"required": true
}
],
"summary": "Create workspace",
"description": "Create a workspace for the current project.",
"responses": {
"200": {
"description": "Workspace created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Workspace"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"branch": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"config": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"type": {
"type": "string",
"const": "worktree"
}
},
"required": ["directory", "type"]
}
]
}
},
"required": ["branch", "config"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.create({\n ...\n})"
}
]
},
"delete": {
"operationId": "experimental.workspace.remove",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "id",
"schema": {
"type": "string",
"pattern": "^wrk.*"
},
"required": true
}
],
"summary": "Remove workspace",
"description": "Remove an existing workspace.",
"responses": {
"200": {
"description": "Workspace removed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Workspace"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.remove({\n ...\n})"
}
]
}
},
"/experimental/workspace": {
"get": {
"operationId": "experimental.workspace.list",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
}
],
"summary": "List workspaces",
"description": "List all workspaces.",
"responses": {
"200": {
"description": "Workspaces",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Workspace"
}
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.list({\n ...\n})"
}
]
}
},
"/experimental/worktree/reset": {
"post": {
"operationId": "worktree.reset",
@@ -8422,6 +8602,85 @@
},
"required": ["type", "properties"]
},
"Event.worktree.ready": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "worktree.ready"
},
"properties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"branch": {
"type": "string"
}
},
"required": ["name", "branch"]
}
},
"required": ["type", "properties"]
},
"Event.worktree.failed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "worktree.failed"
},
"properties": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
},
"required": ["type", "properties"]
},
"Event.workspace.ready": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "workspace.ready"
},
"properties": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": ["name"]
}
},
"required": ["type", "properties"]
},
"Event.workspace.failed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "workspace.failed"
},
"properties": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
},
"required": ["type", "properties"]
},
"Pty": {
"type": "object",
"properties": {
@@ -8535,47 +8794,6 @@
},
"required": ["type", "properties"]
},
"Event.worktree.ready": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "worktree.ready"
},
"properties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"branch": {
"type": "string"
}
},
"required": ["name", "branch"]
}
},
"required": ["type", "properties"]
},
"Event.worktree.failed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "worktree.failed"
},
"properties": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": ["message"]
}
},
"required": ["type", "properties"]
},
"Event": {
"anyOf": [
{
@@ -8689,6 +8907,18 @@
{
"$ref": "#/components/schemas/Event.vcs.branch.updated"
},
{
"$ref": "#/components/schemas/Event.worktree.ready"
},
{
"$ref": "#/components/schemas/Event.worktree.failed"
},
{
"$ref": "#/components/schemas/Event.workspace.ready"
},
{
"$ref": "#/components/schemas/Event.workspace.failed"
},
{
"$ref": "#/components/schemas/Event.pty.created"
},
@@ -8700,12 +8930,6 @@
},
{
"$ref": "#/components/schemas/Event.pty.deleted"
},
{
"$ref": "#/components/schemas/Event.worktree.ready"
},
{
"$ref": "#/components/schemas/Event.worktree.failed"
}
]
},
@@ -10126,6 +10350,46 @@
}
}
},
"Workspace": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^wrk.*"
},
"branch": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"projectID": {
"type": "string"
},
"config": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"type": {
"type": "string",
"const": "worktree"
}
},
"required": ["directory", "type"]
}
]
}
},
"required": ["id", "branch", "projectID", "config"]
},
"WorktreeRemoveInput": {
"type": "object",
"properties": {

View File

@@ -649,6 +649,7 @@
[data-component="reasoning-part"],
[data-component="tool-error"],
[data-component="tool-output"],
[data-component="bash-output"],
[data-component="edit-content"],
[data-component="write-content"],
[data-component="todos"],

View File

@@ -250,6 +250,11 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
icon: "bubble-5",
title: i18n.t("ui.tool.questions"),
}
case "skill":
return {
icon: "brain",
title: input.name || "skill",
}
default:
return {
icon: "mcp",
@@ -1900,3 +1905,25 @@ ToolRegistry.register({
)
},
})
ToolRegistry.register({
name: "skill",
render(props) {
const title = createMemo(() => props.input.name || "skill")
const running = createMemo(() => props.status === "pending" || props.status === "running")
const titleContent = () => <TextShimmer text={title()} active={running()} />
const trigger = () => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title" class="capitalize agent-title">
{titleContent()}
</span>
</div>
</div>
)
return <BasicTool icon="brain" status={props.status} trigger={trigger()} hideDetails />
},
})

View File

@@ -224,7 +224,7 @@ export default defineConfig({
"zh-CN": "使用",
"zh-TW": "使用",
},
items: ["tui", "cli", "web", "ide", "zen", "share", "github", "gitlab"],
items: ["go", "tui", "cli", "web", "ide", "zen", "share", "github", "gitlab"],
},
{

View File

@@ -496,6 +496,7 @@ opencode run "Hello world"
- `auto` - ضغط الجلسة تلقائيًا عند امتلاء السياق (الافتراضي: `true`).
- `prune` - إزالة مخرجات الأدوات القديمة لتوفير الرموز (tokens) (الافتراضي: `true`).
- `reserved` - مخزن مؤقت للرموز (tokens) من أجل الضغط. يترك نافذة كافية لتجنب الفيضان أثناء الضغط.
---

View File

@@ -79,6 +79,32 @@ export const multiply = tool({
---
#### تضارب الأسماء مع الأدوات المدمجة
تُصنّف الأدوات المخصصة حسب اسم الأداة. إذا استخدمت أداة مخصصة نفس اسم أداة مدمجة، فإن الأداة المخصصة تأخذ الأولوية.
على سبيل المثال، يستبدل هذا الملف أداة `bash` المدمجة:
```ts title=".opencode/tools/bash.ts"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Restricted bash wrapper",
args: {
command: tool.schema.string(),
},
async execute(args) {
return `blocked: ${args.command}`
},
})
```
:::note
فضّل استخدام أسماء فريدة ما لم تكن تريد استبدال أداة مدمجة عمدا. إذا كنت تريد تعطيل أداة مدمجة ولكن لا تريد استبدالها، استخدم [الأذونات](/docs/permissions).
:::
---
### الوسائط
يمكنك استخدام `tool.schema`، وهو في الأساس [Zod](https://zod.dev)، لتعريف أنواع الوسائط.

View File

@@ -182,7 +182,7 @@ description: يتكامل OpenCode مع خوادم LSP لديك.
يوفر PHP Intelephense ميزات مدفوعة عبر مفتاح ترخيص. يمكنك تزويده بمفتاح الترخيص عبر وضع (فقط) المفتاح داخل ملف نصي في:
- على macOS/Linux: `$HOME/intelephense/licence.txt`
- على Windows: `%USERPROFILE%/intelephense/licence.txt`
- على macOS/Linux: `$HOME/intelephense/license.txt`
- على Windows: `%USERPROFILE%/intelephense/license.txt`
يجب أن يحتوي الملف على مفتاح الترخيص فقط دون أي محتوى إضافي.

View File

@@ -83,6 +83,37 @@ OpenCode Zen هي قائمة نماذج يوفّرها فريق OpenCode وقد
---
## OpenCode Go
OpenCode Go هي خطة اشتراك منخفضة التكلفة توفّر وصولا موثوقا إلى نماذج البرمجة المفتوحة الشهيرة المقدّمة من فريق OpenCode، والتي تم اختبارها والتحقق من أنها تعمل بشكل جيد مع OpenCode.
1. شغّل الأمر `/connect` في TUI، واختر `OpenCode Go`، ثم انتقل إلى [opencode.ai/auth](https://opencode.ai/zen).
```txt
/connect
```
2. سجّل الدخول، وأضف تفاصيل الفوترة، ثم انسخ مفتاح API الخاص بك.
3. الصق مفتاح API.
```txt
┌ API key
└ enter
```
4. شغّل `/models` في TUI لعرض قائمة النماذج التي نوصي بها.
```txt
/models
```
يعمل مثل أي مزوّد آخر في OpenCode واستخدامه اختياري بالكامل.
---
## الدليل
لنلقِ نظرة على بعض المزوّدات بالتفصيل. إذا رغبت في إضافة مزوّد إلى القائمة،
@@ -1476,6 +1507,39 @@ OpenCode Zen هي قائمة من النماذج التي تم اختبارها
---
### STACKIT
توفّر خدمة STACKIT AI Model Serving بيئة استضافة سيادية مُدارة بالكامل لنماذج الذكاء الاصطناعي، مع التركيز على نماذج LLM مثل Llama وMistral وQwen، مع أقصى درجات سيادة البيانات على بنية تحتية أوروبية.
1. توجّه إلى [STACKIT Portal](https://portal.stackit.cloud)، وانتقل إلى **AI Model Serving**، وأنشئ رمز مصادقة (auth token) لمشروعك.
:::tip
تحتاج إلى حساب عميل STACKIT وحساب مستخدم ومشروع قبل إنشاء رموز المصادقة.
:::
2. شغّل الأمر `/connect` وابحث عن **STACKIT**.
```txt
/connect
```
3. أدخل رمز مصادقة STACKIT AI Model Serving.
```txt
┌ API key
└ enter
```
4. شغّل الأمر `/models` للاختيار من النماذج المتاحة مثل _Qwen3-VL 235B_ أو _Llama 3.3 70B_.
```txt
/models
```
---
### OVHcloud AI Endpoints
1. توجّه إلى [OVHcloud panel](https://ovh.com/manager). انتقل إلى قسم `Public Cloud`، ثم `AI & Machine Learning` > `AI Endpoints`، وفي تبويب `API Keys` انقر **Create a new API key**.

View File

@@ -117,6 +117,78 @@ try {
---
## المخرجات المنظمة
يمكنك طلب مخرجات JSON منظمة من النموذج عن طريق تحديد `format` مع مخطط JSON. سيستخدم النموذج أداة `StructuredOutput` لإرجاع JSON تم التحقق من صحته ومطابق للمخطط الخاص بك.
### الاستخدام الأساسي
```typescript
const result = await client.session.prompt({
path: { id: sessionId },
body: {
parts: [{ type: "text", text: "Research Anthropic and provide company info" }],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: { type: "string", description: "Company name" },
founded: { type: "number", description: "Year founded" },
products: {
type: "array",
items: { type: "string" },
description: "Main products",
},
},
required: ["company", "founded"],
},
},
},
})
// Access the structured output
console.log(result.data.info.structured_output)
// { company: "Anthropic", founded: 2021, products: ["Claude", "Claude API"] }
```
### أنواع صيغ الإخراج
| النوع | الوصف |
| ------------- | -------------------------------------------------- |
| `text` | الافتراضي. استجابة نصية قياسية (بدون مخرجات منظمة) |
| `json_schema` | يرجع JSON تم التحقق من صحته ومطابق للمخطط المقدم |
### صيغة مخطط JSON
عند استخدام `type: 'json_schema'`، يجب تقديم:
| الحقل | النوع | الوصف |
| ------------ | --------------- | ------------------------------------------------ |
| `type` | `'json_schema'` | مطلوب. يحدد وضع مخطط JSON |
| `schema` | `object` | مطلوب. كائن مخطط JSON الذي يحدد بنية الإخراج |
| `retryCount` | `number` | اختياري. عدد محاولات إعادة التحقق (الافتراضي: 2) |
### معالجة الأخطاء
إذا فشل النموذج في إنتاج مخرجات منظمة صالحة بعد جميع المحاولات، ستتضمن الاستجابة `StructuredOutputError`:
```typescript
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Failed to produce structured output:", result.data.info.error.message)
console.error("Attempts:", result.data.info.error.retries)
}
```
### أفضل الممارسات
1. **قدّم أوصافا واضحة** في خصائص المخطط لمساعدة النموذج على فهم البيانات التي يجب استخراجها
2. **استخدم `required`** لتحديد الحقول التي يجب أن تكون موجودة
3. **حافظ على تركيز المخططات** - المخططات المتداخلة المعقدة قد تكون أصعب على النموذج لملئها بشكل صحيح
4. **عيّن `retryCount` مناسبا** - قم بزيادته للمخططات المعقدة، وتقليله للمخططات البسيطة
---
## APIs
توفر SDK جميع واجهات الخادم عبر عميل آمن للأنواع.
@@ -226,27 +298,27 @@ const { providers, default: defaults } = await client.config.providers()
### الجلسات (`session`)
| الطريقة | الوصف | ملاحظات |
| ---------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | سرد الجلسات | يعيد <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | جلب جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | سرد الجلسات الفرعية | يعيد <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | إنشاء جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | حذف جلسة | يعيد `boolean` |
| `session.update({ path, body })` | تحديث خصائص الجلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | تحليل التطبيق وإنشاء `AGENTS.md` | يعيد `boolean` |
| `session.abort({ path })` | إيقاف جلسة قيد التشغيل | يعيد `boolean` |
| `session.share({ path })` | مشاركة جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | إلغاء مشاركة جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | تلخيص جلسة | يعيد `boolean` |
| `session.messages({ path })` | سرد الرسائل في جلسة | يعيد `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | جلب تفاصيل الرسالة | يعيد `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | إرسال رسالة مطالبة | `body.noReply: true` يعيد UserMessage (للسياق فقط). الافتراضي يعيد <a href={typesUrl}><code>AssistantMessage</code></a> مع استجابة AI |
| `session.command({ path, body })` | إرسال أمر إلى الجلسة | يعيد `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | تشغيل أمر shell | يعيد <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | التراجع عن رسالة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | استعادة الرسائل المتراجع عنها | يعيد <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | الاستجابة لطلب إذن | يعيد `boolean` |
| الطريقة | الوصف | ملاحظات |
| ---------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `session.list()` | سرد الجلسات | يعيد <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | جلب جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | سرد الجلسات الفرعية | يعيد <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | إنشاء جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | حذف جلسة | يعيد `boolean` |
| `session.update({ path, body })` | تحديث خصائص الجلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | تحليل التطبيق وإنشاء `AGENTS.md` | يعيد `boolean` |
| `session.abort({ path })` | إيقاف جلسة قيد التشغيل | يعيد `boolean` |
| `session.share({ path })` | مشاركة جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | إلغاء مشاركة جلسة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | تلخيص جلسة | يعيد `boolean` |
| `session.messages({ path })` | سرد الرسائل في جلسة | يعيد `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | جلب تفاصيل الرسالة | يعيد `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | إرسال رسالة مطالبة | `body.noReply: true` يعيد UserMessage (للسياق فقط). الافتراضي يعيد <a href={typesUrl}><code>AssistantMessage</code></a> مع استجابة AI. يدعم `body.outputFormat` من أجل [المخرجات المنظمة](#المخرجات-المنظمة) |
| `session.command({ path, body })` | إرسال أمر إلى الجلسة | يعيد `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | تشغيل أمر shell | يعيد <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | التراجع عن رسالة | يعيد <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | استعادة الرسائل المتراجع عنها | يعيد <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | الاستجابة لطلب إذن | يعيد `boolean` |
---

View File

@@ -370,6 +370,9 @@ How is auth handled in @packages/functions/src/api/index.ts?
- `scroll_acceleration` - فعّل تسارع التمرير على نمط macOS لتمرير سلس وطبيعي. عند تفعيله، تزداد سرعة التمرير مع إيماءات التمرير السريعة وتبقى دقيقة للحركات الأبطأ. **يتقدّم هذا الإعداد على `scroll_speed` ويستبدله عند تفعيله.**
- `scroll_speed` - يتحكم في سرعة تمرير واجهة TUI عند استخدام أوامر التمرير (الحد الأدنى: `1`). القيمة الافتراضية هي `3`. **ملاحظة: يتم تجاهل هذا إذا تم ضبط `scroll_acceleration.enabled` على `true`.**
- `diff_style` - يتحكم في عرض الفروقات (diff). القيمة `"auto"` تتكيف مع عرض terminal، و`"stacked"` تعرض عمودًا واحدًا دائمًا.
استخدم `OPENCODE_TUI_CONFIG` لتحميل مسار إعدادات TUI مخصص.
---

View File

@@ -59,6 +59,7 @@ OpenCode Zen هو بوابة للذكاء الاصطناعي تتيح لك ال
| النموذج | معرّف النموذج | نقطة النهاية | حزمة AI SDK |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -68,22 +69,24 @@ OpenCode Zen هو بوابة للذكاء الاصطناعي تتيح لك ال
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 Free | minimax-m2.1-free | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 Free | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -111,29 +114,34 @@ https://opencode.ai/zen/v1/models
| النموذج | الإدخال | الإخراج | قراءة مخزنة | كتابة مخزنة |
| --------------------------------- | ------- | ------- | ----------- | ----------- |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.1 Free | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 4.7 Free | Free | Free | Free | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 Free | Free | Free | Free | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.1 | $1.07 | $8.50 | $0.107 | - |
@@ -152,9 +160,7 @@ https://opencode.ai/zen/v1/models
النماذج المجانية:
- GLM 4.7 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Kimi K2.5 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- MiniMax M2.1 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- MiniMax M2.5 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
<a href={email}>تواصل معنا</a> إذا كانت لديك أي أسئلة.
@@ -183,9 +189,7 @@ https://opencode.ai/zen/v1/models
تتم استضافة جميع نماذجنا في الولايات المتحدة. يلتزم مزوّدونا بسياسة عدم الاحتفاظ بالبيانات (zero-retention) ولا يستخدمون بياناتك لتدريب النماذج، مع الاستثناءات التالية:
- Big Pickle: خلال فترة إتاحته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- GLM 4.7 Free: خلال فترة إتاحته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- Kimi K2.5 Free: خلال فترة إتاحته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- MiniMax M2.1 Free: خلال فترة إتاحته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- MiniMax M2.5 Free: خلال فترة إتاحته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- OpenAI APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [سياسات بيانات OpenAI](https://platform.openai.com/docs/guides/your-data).
- Anthropic APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [سياسات بيانات Anthropic](https://docs.anthropic.com/en/docs/claude-code/data-usage).

View File

@@ -555,6 +555,7 @@ OpenCode se može konfigurirati pomoću varijabli okruženja.
| `OPENCODE_AUTO_SHARE` | boolean | Automatski dijeli sesije |
| `OPENCODE_GIT_BASH_PATH` | string | Putanja do Git Bash izvršne datoteke na Windows-u |
| `OPENCODE_CONFIG` | string | Putanja do konfiguracijskog fajla |
| `OPENCODE_TUI_CONFIG` | string | Putanja do TUI konfiguracijskog fajla |
| `OPENCODE_CONFIG_DIR` | string | Putanja do konfiguracijskog direktorija |
| `OPENCODE_CONFIG_CONTENT` | string | Inline json konfiguracijski sadržaj |
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Onemogući automatske provjere ažuriranja |

View File

@@ -14,10 +14,11 @@ OpenCode podržava i **JSON** i **JSONC** (JSON sa komentarima) formate.
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
// Theme configuration
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-5",
"autoupdate": true,
"server": {
"port": 4096,
},
}
```
@@ -30,8 +31,10 @@ Možete postaviti svoju konfiguraciju na nekoliko različitih lokacija i one ima
:::note
Konfiguracijski fajlovi se **spajaju**, ne zamjenjuju.
:::
Konfiguracijski fajlovi se spajaju, ne zamjenjuju. Kombiniraju se postavke sa sljedećih konfiguracijskih lokacija. Kasnije konfiguracije poništavaju prethodne samo za konfliktne ključeve. Nekonfliktne postavke iz svih konfiguracija su sačuvane.
Na primjer, ako vaša globalna konfiguracija postavlja `theme: "opencode"` i `autoupdate: true`, a vaša projektna konfiguracija postavlja `model: "anthropic/claude-sonnet-4-5"`, konačna konfiguracija će uključivati sve tri postavke.
Na primjer, ako vaša globalna konfiguracija postavlja `autoupdate: true`, a vaša projektna konfiguracija postavlja `model: "anthropic/claude-sonnet-4-5"`, konačna konfiguracija će uključivati obje postavke.
---
@@ -40,23 +43,26 @@ Na primjer, ako vaša globalna konfiguracija postavlja `theme: "opencode"` i `au
Izvori konfiguracije se učitavaju ovim redoslijedom (kasniji izvori poništavaju ranije):
1. **Udaljena konfiguracija** (od `.well-known/opencode`) - organizacijske postavke
2. **Globalna konfiguracija** (`~/.config/opencode/opencode.json`) - korisničke postavke
2. **Globalna konfiguracija** (`~/.config/opencode/opencode.json`) - korisničke preferencije
3. **Prilagođena konfiguracija** (`OPENCODE_CONFIG` env var) - prilagođena preinačenja
4. **Konfiguracija projekta** (`opencode.json` u projektu) - postavke specifične za projekat
5. **`.opencode` direktoriji** - agenti, komande, dodaci
6. **Inline konfiguracija** (`OPENCODE_CONFIG_CONTENT` env var) - runtime preinačenja
To znači da konfiguracije projekta mogu nadjačati globalne zadane postavke, a globalne konfiguracije mogu nadjačati postavke udaljene organizacije.
:::note
Direktoriji `.opencode` i `~/.config/opencode` koriste **imena u množini** za poddirektorije: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/` i `themes/`. Pojedinačna imena (npr. `agent/`) su također podržana za kompatibilnost unatrag.
:::
To znači da konfiguracije projekta mogu nadjačati globalne zadane postavke, a globalne konfiguracije mogu nadjačati postavke udaljene organizacije.
:::note
Direktoriji `.opencode` i `~/.config/opencode` koriste **imena u množini** za poddirektorije: `agents/`, `commands/`, `modes/`, `plugins/`, `skills/`, `tools/` i `themes/`. Pojedinačna imena (npr. `agent/`) su također podržana za kompatibilnost unatrag.
:::
---
### Udaljeno (Remote)
Organizacije mogu pružiti zadanu konfiguraciju preko `.well-known/opencode` krajnje tačke. Ovo se automatski preuzima kada se autentifikujete kod provajdera koji to podržava.
Prvo se učitava udaljena konfiguracija koja služi kao osnovni sloj. Svi ostali izvori konfiguracije (globalni, projektni) mogu nadjačati ove zadane postavke.
Na primjer, ako vaša organizacija nudi MCP servere koji su po defaultu onemogućeni:
```json title="Remote config from .well-known/opencode"
@@ -89,7 +95,10 @@ Možete omogućiti određene servere u vašoj lokalnoj konfiguraciji:
### Globalno
Postavite svoju globalnu OpenCode konfiguraciju u `~/.config/opencode/opencode.json`. Koristite globalnu konfiguraciju za korisničke preferencije kao što su teme, provajderi ili veze tipki.
Postavite svoju globalnu OpenCode konfiguraciju u `~/.config/opencode/opencode.json`. Koristite globalnu konfiguraciju za korisničke preferencije kao što su provajderi, modeli i dozvole.
Za postavke specifične za TUI, koristite `~/.config/opencode/tui.json`.
Globalna konfiguracija poništava zadane postavke udaljene organizacije.
---
@@ -97,10 +106,15 @@ Globalna konfiguracija poništava zadane postavke udaljene organizacije.
### Projekt
Dodajte `opencode.json` u korijen projekta. Konfiguracija projekta ima najveći prioritet među standardnim konfiguracijskim datotekama - ona nadjačava globalne i udaljene konfiguracije.
Za TUI postavke specifične za projekat, dodajte `tui.json` pored njega.
:::tip
Postavite specifičnu konfiguraciju projekta u korijen vašeg projekta.
:::
Kada se OpenCode pokrene, traži konfiguracijsku datoteku u trenutnom direktoriju ili prelazi do najbližeg Git direktorija.
Ovo je također sigurno provjeriti u Git i koristi istu shemu kao globalna.
---
@@ -134,33 +148,33 @@ Prilagođeni direktorij se učitava nakon direktorija globalne konfiguracije i `
## Šema
Konfiguracijski fajl ima šemu koja je definirana u [**`opencode.ai/config.json`**](https://opencode.ai/config.json).
TUI konfiguracija koristi [**`opencode.ai/tui.json`**](https://opencode.ai/tui.json).
Vaš editor bi trebao biti u mogućnosti da validira i autodovršava na osnovu šeme.
---
### TUI
Možete konfigurirati postavke specifične za TUI putem opcije `tui`.
Koristite namjenski `tui.json` (ili `tui.jsonc`) fajl za postavke specifične za TUI.
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"tui": {
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
}
"$schema": "https://opencode.ai/tui.json",
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
}
```
Dostupne opcije:
Koristite `OPENCODE_TUI_CONFIG` da pokažete na prilagođeni TUI konfiguracijski fajl.
- `scroll_acceleration.enabled` - Omogući ubrzanje skrolovanja u macOS stilu. **Ima prednost nad `scroll_speed`.**
- `scroll_speed` - Prilagođeni množitelj brzine pomicanja (podrazumevano: `3`, minimalno: `1`). Zanemareno ako je `scroll_acceleration.enabled` `true`.
- `diff_style` - Kontrola prikaza razlike. `"auto"` se prilagođava širini terminala, `"stacked"` uvijek prikazuje jednu kolonu.
[Saznajte više o korištenju TUI](/docs/tui) ovdje.
Stari `theme`, `keybinds`, i `tui` ključevi u `opencode.json` su zastarjeli i automatski će se migrirati kada je to moguće.
[Saznajte više o korištenju TUI ovdje](/docs/tui#configure).
---
@@ -188,7 +202,8 @@ Dostupne opcije:
- `mdns` - Omogući mDNS otkrivanje servisa. Ovo omogućava drugim uređajima na mreži da otkriju vaš OpenCode server.
- `mdnsDomain` - Prilagođeno ime domene za mDNS servis. Zadano je `opencode.local`. Korisno za pokretanje više instanci na istoj mreži.
- `cors` - Dodatni origini koji omogućavaju CORS kada koristite HTTP server iz klijenta baziranog na pretraživaču. Vrijednosti moraju biti puni origin (shema + host + opcijski port), npr. `https://app.example.com`.
[Saznajte više o serveru](/docs/server) ovdje.
[Saznajte više o serveru ovdje](/docs/server).
---
@@ -206,7 +221,7 @@ Možete upravljati alatima koje LLM može koristiti putem opcije `tools`.
}
```
[Saznajte više o alatima](/docs/tools) ovdje.
[Saznajte više o alatima ovdje](/docs/tools).
---
@@ -224,6 +239,7 @@ Možete konfigurirati dobavljače i modele koje želite koristiti u svojoj OpenC
```
Opcija `small_model` konfigurira poseban model za lagane zadatke poput generiranja naslova. Podrazumevano, OpenCode pokušava da koristi jeftiniji model ako je dostupan od vašeg provajdera, inače se vraća na vaš glavni model.
Opcije provajdera mogu uključivati `timeout` i `setCacheKey`:
```json title="opencode.json"
@@ -242,7 +258,8 @@ Opcije provajdera mogu uključivati `timeout` i `setCacheKey`:
- `timeout` - Vrijeme čekanja zahtjeva u milisekundama (podrazumevano: 300000). Postavite na `false` da onemogućite.
- `setCacheKey` - Osigurajte da je ključ keš memorije uvijek postavljen za određenog provajdera.
Također možete konfigurirati [lokalne modele](/docs/models#local). [Saznajte više](/docs/models).
Također možete konfigurirati [lokalne modele](/docs/models#local). [Saznajte više](/docs/models).
---
@@ -272,21 +289,23 @@ Amazon Bedrock podržava konfiguraciju specifičnu za AWS:
- `region` - AWS regija za Bedrock (zadano na `AWS_REGION` env var ili `us-east-1`)
- `profile` - AWS imenovani profil iz `~/.aws/credentials` (zadano na `AWS_PROFILE` env var)
- `endpoint` - URL prilagođene krajnje tačke za VPC krajnje tačke. Ovo je alias za generičku opciju `baseURL` koristeći terminologiju specifičnu za AWS. Ako su oba navedena, `endpoint` ima prednost.
:::note
Tokeni nosioca (`AWS_BEARER_TOKEN_BEDROCK` ili `/connect`) imaju prednost nad autentifikacijom zasnovanom na profilu. Pogledajte [prednost autentifikacije](/docs/providers#authentication-precedence) za detalje.
:::
[Saznajte više o konfiguraciji Amazon Bedrock](/docs/providers#amazon-bedrock).
:::note
Tokeni nosioca (`AWS_BEARER_TOKEN_BEDROCK` ili `/connect`) imaju prednost nad autentifikacijom zasnovanom na profilu. Pogledajte [prednost autentifikacije](/docs/providers#authentication-precedence) za detalje.
:::
[Saznajte više o konfiguraciji Amazon Bedrock](/docs/providers#amazon-bedrock).
---
### Tema
### Teme
Možete konfigurirati temu koju želite koristiti u svojoj OpenCode konfiguraciji putem opcije `theme`.
Postavite vašu UI temu u `tui.json`.
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"theme": ""
"$schema": "https://opencode.ai/tui.json",
"theme": "tokyonight"
}
```
@@ -332,6 +351,7 @@ Možete postaviti zadanog agenta koristeći opciju `default_agent`. Ovo određuj
```
Zadani agent mora biti primarni agent (ne podagent). Ovo može biti ugrađeni agent kao što je `"build"` ili `"plan"`, ili [prilagođeni agent](/docs/agents) koji ste definirali. Ako navedeni agent ne postoji ili je podagent, OpenCode će se vratiti na `"build"` s upozorenjem.
Ova postavka se primjenjuje na sva sučelja: TUI, CLI (`opencode run`), desktop aplikaciju i GitHub Action.
---
@@ -352,7 +372,8 @@ Ovo prihvata:
- `"manual"` - Dozvoli ručno dijeljenje putem naredbi (podrazumevano)
- `"auto"` - Automatski dijelite nove razgovore
- `"disabled"` - Onemogući dijeljenje u potpunosti
Podrazumevano, dijeljenje je postavljeno na ručni način rada gdje trebate eksplicitno dijeliti razgovore pomoću naredbe `/share`.
Podrazumevano, dijeljenje je postavljeno na ručni način rada gdje trebate eksplicitno dijeliti razgovore pomoću naredbe `/share`.
---
@@ -384,11 +405,11 @@ Također možete definirati naredbe koristeći markdown fajlove u `~/.config/ope
### Prečice tipki
Možete prilagoditi svoje veze tipki putem opcije `keybinds`.
Prilagodite prečice tipki u `tui.json`.
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"keybinds": {}
}
```
@@ -442,6 +463,7 @@ Možete konfigurirati formatere koda putem opcije `formatter`.
### Dozvole
Prema zadanim postavkama, OpenCode **dopušta sve operacije** bez potrebe za eksplicitnim dopuštenjem. Ovo možete promijeniti koristeći opciju `permission`.
Na primjer, da osigurate da alati `edit` i `bash` zahtijevaju odobrenje korisnika:
```json title="opencode.json"
@@ -467,13 +489,15 @@ Možete kontrolirati ponašanje sažimanja konteksta putem opcije `compaction`.
"$schema": "https://opencode.ai/config.json",
"compaction": {
"auto": true,
"prune": true
"prune": true,
"reserved": 10000
}
}
```
- `auto` - Automatski sažimanje sesije kada je kontekst pun (podrazumevano: `true`).
- `prune` - Uklonite stare izlaze alata da sačuvate tokene (podrazumevano: `true`).
- `reserved` - Token buffer za sažimanje. Ostavlja dovoljno prostora da se izbjegne prelijevanje tokom sažimanja
---
@@ -512,6 +536,7 @@ Možete konfigurirati MCP servere koje želite koristiti putem opcije `mcp`.
### Dodaci
[Plugins](/docs/plugins) proširuju OpenCode sa prilagođenim alatima, kukicama i integracijama.
Postavite datoteke dodataka u `.opencode/plugins/` ili `~/.config/opencode/plugins/`. Također možete učitati dodatke iz npm-a preko opcije `plugin`.
```json title="opencode.json"
@@ -554,6 +579,7 @@ Možete onemogućiti dobavljače koji se automatski učitavaju preko opcije `dis
:::note
`disabled_providers` ima prioritet nad `enabled_providers`.
:::
Opcija `disabled_providers` prihvata niz ID-ova provajdera. Kada je provajder onemogućen:
- Neće se učitati čak i ako su varijable okruženja postavljene.
@@ -574,9 +600,11 @@ Možete odrediti listu dozvoljenih dobavljača putem opcije `enabled_providers`.
```
Ovo je korisno kada želite da ograničite OpenCode da koristi samo određene provajdere umjesto da ih onemogućavate jednog po jednog.
:::note
`disabled_providers` ima prioritet nad `enabled_providers`.
:::
Ako se provajder pojavljuje i u `enabled_providers` i `disabled_providers`, `disabled_providers` ima prioritet za kompatibilnost unatrag.
---
@@ -649,7 +677,9 @@ Putanja fajla mogu biti:
- U odnosu na direktorij konfiguracijskih datoteka
- Ili apsolutne staze koje počinju sa `/` ili `~`
Ovo je korisno za:
Ovo je korisno za:
- Pohranjivanje osjetljivih podataka poput API ključeva u odvojenim datotekama.
- Uključujući velike datoteke instrukcija bez zatrpavanja vaše konfiguracije.
- Dijeljenje zajedničkih isječaka konfiguracije u više konfiguracijskih datoteka.

View File

@@ -79,6 +79,32 @@ Ovo stvara dva alata: `math_add` i `math_multiply`.
---
#### Sukob imena s ugrađenim alatima
Prilagođeni alati su prepoznati po imenu. Ako prilagođeni alat koristi isto ime kao ugrađeni alat, prilagođeni alat ima prednost.
Na primjer, ova datoteka zamjenjuje ugrađeni `bash` alat:
```ts title=".opencode/tools/bash.ts"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Restricted bash wrapper",
args: {
command: tool.schema.string(),
},
async execute(args) {
return `blocked: ${args.command}`
},
})
```
:::note
Preferirajte jedinstvena imena osim ako namjerno ne želite zamijeniti ugrađeni alat. Ako želite onemogućiti ugrađeni alat, ali ne i nadjačati ga, koristite [dozvole](/docs/permissions).
:::
---
### Argumenti
Možete koristiti `tool.schema`, što je samo [Zod](https://zod.dev), da definirate tipove argumenata.
@@ -166,4 +192,4 @@ export default tool({
})
```
Ovdje koristimo [`Bun.$`\_](https://bun.com/docs/runtime/shell) uslužni program za pokretanje Python skripte.
Ovdje koristimo [`Bun.$`](https://bun.com/docs/runtime/shell) uslužni program za pokretanje Python skripte.

View File

@@ -80,9 +80,10 @@ Također ga možete instalirati pomoću sljedećih naredbi:
- **Korištenje Parua na Arch Linuxu**
```bash
paru -S opencode-bin
```
```bash
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
```
#### Windows

View File

@@ -3,11 +3,11 @@ title: Prečice tipki
description: Prilagodite svoje veze tipki.
---
OpenCode ima listu veza tipki koje možete prilagoditi preko OpenCode konfiguracije.
OpenCode ima listu veza tipki koje možete prilagoditi putem `tui.json`.
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"leader": "ctrl+x",
"app_exit": "ctrl+c,ctrl+d,<leader>q",
@@ -108,18 +108,20 @@ OpenCode ima listu veza tipki koje možete prilagoditi preko OpenCode konfigurac
## Leader tipka
OpenCode koristi `leader` (vodeću) tipku za većinu povezivanja tipki. Ovo izbjegava sukobe u vašem terminalu.
Prema zadanim postavkama, `ctrl+x` je vodeća tipka i većina radnji zahtijeva da prvo pritisnete vodeću tipku, a zatim i prečicu. Na primjer, da biste započeli novu sesiju, prvo pritisnite `ctrl+x`, a zatim pritisnite `n`.
Ne morate koristiti vodeću tipku za svoje veze tipki, ali preporučujemo da to učinite.
---
## Onemogućavanje prečica tipki
Možete onemogućiti spajanje tipki dodavanjem ključa u svoju konfiguraciju s vrijednošću "none".
Možete onemogućiti spajanje tipki dodavanjem ključa u `tui.json` s vrijednošću "none".
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"session_compact": "none"
}

View File

@@ -8,40 +8,43 @@ OpenCode se integriše sa vašim Language Server Protocol (LSP) serverima kako b
## Ugrađeni
OpenCode dolazi sa nekoliko ugrađenih LSP servera za popularne jezike:
| LSP server | Ekstenzije | Zahtjevi
|------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------ |
| astro | .astro | Automatske instalacije za Astro projekte |
| bash | .sh, .bash, .zsh, .ksh | Automatski instalira bash-language-server |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Automatske instalacije za C/C++ projekte |
| csharp | .cs | `.NET SDK` instaliran |
| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` komanda dostupna |
| dart | .dart | `dart` komanda dostupna |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` komanda dostupna (automatski detektuje deno.json/deno.jsonc) |
| elixir-ls | .ex, .exs | `elixir` komanda dostupna |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` ovisnost u projektu |
| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` instaliran |
| gleam | .bleam | `gleam` komanda dostupna |
| gopls | .go | `go` komanda dostupna |
| hls | .hs, .lhs | `haskell-language-server-wrapper` komanda dostupna |
| jdtls | .java | `Java SDK (version 21+)` instaliran |
| kotlin-ls | .kt, .kts | Automatske instalacije za Kotlin projekte |
| lua-ls | .lua | Automatske instalacije za Lua projekte |
| nixd | .nix | `nixd` komanda dostupna |
| ocaml-lsp | .ml, .mli | `ocamllsp` komanda dostupna |
| oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | `oxlint` zavisnost u projektu |
| php intelephense | .php | Automatske instalacije za PHP projekte |
| prisma | .prisma | `prisma` komanda dostupna |
| pyright | .py, .pyi | `pyright` ovisnost instalirana |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` i `gem` komande dostupne |
| rust | .rs | `rust-analyzer` komanda dostupna |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` instaliran (`xcode` na macOS-u) |
| svelte | .svelte | Automatske instalacije za Svelte projekte |
| terraform | .tf, .tfvars | Automatske instalacije iz GitHub izdanja |
| tinymist | .typ, .typc | Automatske instalacije iz GitHub izdanja |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` zavisnost u projektu |
| vue | .vue | Automatske instalacije za Vue projekte |
| yaml-ls | .yaml, .yml | Automatski instalira Red Hat yaml-language-server |
| zls | .zig, .zon | `zig` komanda dostupna |
| LSP server | Ekstenzije | Zahtjevi |
| ------------------ | ------------------------------------------------------------------- | -------------------------------------------------------- |
| astro | .astro | Automatske instalacije za Astro projekte |
| bash | .sh, .bash, .zsh, .ksh | Automatski instalira bash-language-server |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Automatske instalacije za C/C++ projekte |
| csharp | .cs | `.NET SDK` instaliran |
| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` komanda dostupna |
| dart | .dart | `dart` komanda dostupna |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` komanda dostupna (automatski detektuje deno.json) |
| elixir-ls | .ex, .exs | `elixir` komanda dostupna |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` ovisnost u projektu |
| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` instaliran |
| gleam | .gleam | `gleam` komanda dostupna |
| gopls | .go | `go` komanda dostupna |
| hls | .hs, .lhs | `haskell-language-server-wrapper` komanda dostupna |
| jdtls | .java | `Java SDK (version 21+)` instaliran |
| julials | .jl | `julia` i `LanguageServer.jl` instalirani |
| kotlin-ls | .kt, .kts | Automatske instalacije za Kotlin projekte |
| lua-ls | .lua | Automatske instalacije za Lua projekte |
| nixd | .nix | `nixd` komanda dostupna |
| ocaml-lsp | .ml, .mli | `ocamllsp` komanda dostupna |
| oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | `oxlint` zavisnost u projektu |
| php intelephense | .php | Automatske instalacije za PHP projekte |
| prisma | .prisma | `prisma` komanda dostupna |
| pyright | .py, .pyi | `pyright` ovisnost instalirana |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` i `gem` komande dostupne |
| rust | .rs | `rust-analyzer` komanda dostupna |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` instaliran (`xcode` na macOS-u) |
| svelte | .svelte | Automatske instalacije za Svelte projekte |
| terraform | .tf, .tfvars | Automatske instalacije iz GitHub izdanja |
| tinymist | .typ, .typc | Automatske instalacije iz GitHub izdanja |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` zavisnost u projektu |
| vue | .vue | Automatske instalacije za Vue projekte |
| yaml-ls | .yaml, .yml | Automatski instalira Red Hat yaml-language-server |
| zls | .zig, .zon | `zig` komanda dostupna |
LSP serveri su automatski omogućeni kada se otkrije jedna od gore navedenih ekstenzija datoteke i zahtjevi su ispunjeni.
:::note
Možete onemogućiti automatska preuzimanja LSP servera tako što ćete postaviti varijablu okruženja `OPENCODE_DISABLE_LSP_DOWNLOAD` na `true`.
@@ -70,13 +73,15 @@ Možete prilagoditi LSP servere kroz `lsp` odjeljak u vašoj opencode konfigurac
```
Svaki LSP server podržava sljedeće:
| Svojstvo | Vrsta | Opis
|---------------- | -------- | ------------------------------------------------- |
| `disabled` | boolean | Postavite ovo na `true` da onemogućite LSP server |
| `command` | string[] | Naredba za pokretanje LSP servera |
| `extensions` | string[] | Ekstenzije datoteka koje ovaj LSP server treba da rukuje |
| `env` | objekt | Varijable okruženja koje treba postaviti prilikom pokretanja servera |
| `initialization` | objekt | Opcije inicijalizacije za slanje na LSP server |
| Svojstvo | Vrsta | Opis |
| ---------------- | -------- | -------------------------------------------------------------------- |
| `disabled` | boolean | Postavite ovo na `true` da onemogućite LSP server |
| `command` | string[] | Naredba za pokretanje LSP servera |
| `extensions` | string[] | Ekstenzije datoteka koje ovaj LSP server treba da rukuje |
| `env` | objekt | Varijable okruženja koje treba postaviti prilikom pokretanja servera |
| `initialization` | objekt | Opcije inicijalizacije za slanje na LSP server |
Pogledajmo neke primjere.
---

View File

@@ -225,7 +225,7 @@ export const NotificationPlugin = async ({ project, client, $, directory, worktr
Koristimo `osascript` za pokretanje AppleScript-a na macOS-u. Ovdje ga koristimo za slanje obavještenja.
:::note
Ako koristite desktop aplikaciju OpenCode, ona može automatski slati sistemske obavijesti kada je odgovor spreman ili kada dođe do greške u sesiji.
Ako alat dodatka koristi isto ime kao ugrađeni alat, alat dodatka ima prednost.
:::
---

View File

@@ -84,6 +84,37 @@ Radi kao i svaki drugi provajder u OpenCode i potpuno je opcionalan za korišten
---
## OpenCode Go
OpenCode Go je jeftin plan pretplate koji pruža pouzdan pristup popularnim modelima otvorenog kodiranja koje pruža OpenCode tim i koji su testirani i verificirani da dobro rade s OpenCode-om.
1. Pokrenite naredbu `/connect` u TUI-u, odaberite `OpenCode Go` i idite na [opencode.ai/auth](https://opencode.ai/zen).
```txt
/connect
```
2. Prijavite se, dodajte svoje detalje naplate i kopirajte svoj API ključ.
3. Zalijepite svoj API ključ.
```txt
┌ API key
└ enter
```
4. Pokrenite naredbu `/models` u TUI da vidite listu modela koje preporučujemo.
```txt
/models
```
Radi kao i svaki drugi provajder u OpenCode i potpuno je opcionalan za korištenje.
---
## Direktorij
Pogledajmo neke od provajdera detaljno. Ako želite dodati provajdera na
@@ -1479,9 +1510,42 @@ Ove postavke su opcione i treba ih konfigurirati u skladu s vašim SAP AI Core p
5. Pokrenite naredbu `/models` da odaberete između 40+ dostupnih modela.
```txt
```txt
/models
```
```
---
### STACKIT
STACKIT AI Model Serving pruža potpuno upravljano suvereno hosting okruženje za AI modele, fokusirajući se na LLM-ove kao što su Llama, Mistral i Qwen, uz maksimalan suverenitet podataka na evropskoj infrastrukturi.
1. Idite na [STACKIT Portal](https://portal.stackit.cloud), idite na **AI Model Serving** i kreirajte token za autentifikaciju za svoj projekat.
:::tip
Potreban vam je STACKIT korisnički račun, korisnički nalog i projekat prije kreiranja tokena za autentifikaciju.
:::
2. Pokrenite naredbu `/connect` i potražite **STACKIT**.
```txt
/connect
```
3. Unesite svoj STACKIT AI Model Serving token za autentifikaciju.
```txt
┌ API key
└ enter
```
4. Pokrenite naredbu `/models` da odaberete dostupne modele kao što su _Qwen3-VL 235B_ ili _Llama 3.3 70B_.
```txt
/models
```
---

View File

@@ -117,6 +117,78 @@ try {
---
## Strukturirani izlaz
Možete zatražiti strukturirani JSON izlaz od modela specificiranjem `format` sa JSON šemom. Model će koristiti `StructuredOutput` alat da vrati validirani JSON koji odgovara vašoj šemi.
### Osnovna upotreba
```typescript
const result = await client.session.prompt({
path: { id: sessionId },
body: {
parts: [{ type: "text", text: "Research Anthropic and provide company info" }],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: { type: "string", description: "Company name" },
founded: { type: "number", description: "Year founded" },
products: {
type: "array",
items: { type: "string" },
description: "Main products",
},
},
required: ["company", "founded"],
},
},
},
})
// Access the structured output
console.log(result.data.info.structured_output)
// { company: "Anthropic", founded: 2021, products: ["Claude", "Claude API"] }
```
### Tipovi formata izlaza
| Tip | Opis |
| ------------- | ------------------------------------------------------------------- |
| `text` | Default. Standardni tekstualni odgovor (nema strukturiranog izlaza) |
| `json_schema` | Vraća validirani JSON koji odgovara pruženoj šemi |
### Format JSON šeme
Kada koristite `type: 'json_schema'`, navedite:
| Polje | Tip | Opis |
| ------------ | --------------- | ----------------------------------------------------------- |
| `type` | `'json_schema'` | Obavezno. Određuje JSON schema način rada |
| `schema` | `object` | Obavezno. JSON Schema objekt koji definira strukturu izlaza |
| `retryCount` | `number` | Opcionalno. Broj ponovnih pokušaja validacije (default: 2) |
### Rukovanje greškama
Ako model ne uspije proizvesti validan strukturirani izlaz nakon svih ponovnih pokušaja, odgovor će uključivati `StructuredOutputError`:
```typescript
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Failed to produce structured output:", result.data.info.error.message)
console.error("Attempts:", result.data.info.error.retries)
}
```
### Najbolje prakse
1. **Navedite jasne opise** u svojstvima vaše šeme kako biste pomogli modelu da razumije koje podatke treba izdvojiti
2. **Koristite `required`** da odredite koja polja moraju biti prisutna
3. **Držite šeme fokusiranim** - složene ugniježđene šeme modelu mogu biti teže za ispravno popunjavanje
4. **Postavite odgovarajući `retryCount`** - povećajte za složene šeme, smanjite za jednostavne
---
## API-ji
SDK izlaže sve server API-je kroz type-safe klijent.
@@ -226,27 +298,27 @@ const { providers, default: defaults } = await client.config.providers()
### Sesije
| Metoda | Opis | Napomene |
| ---------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Delete session | Returns `boolean` |
| `session.update({ path, body })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ path })` | Abort a running session | Returns `boolean` |
| `session.share({ path })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
| `session.messages({ path })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns <a href={typesUrl}><code>AssistantMessage</code></a> with AI response |
| `session.command({ path, body })` | Send command to session | Returns `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
| Metoda | Opis | Napomene |
| ---------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Delete session | Returns `boolean` |
| `session.update({ path, body })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ path })` | Abort a running session | Returns `boolean` |
| `session.share({ path })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
| `session.messages({ path })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns <a href={typesUrl}><code>AssistantMessage</code></a> with AI response. Supports `body.outputFormat` for [structured output](#strukturirani-izlaz) |
| `session.command({ path, body })` | Send command to session | Returns `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
---

View File

@@ -86,116 +86,116 @@ opencode server izlaže sljedece API-je.
### Globalno
| Metoda | Putanja | Opis | Odgovor |
| ------ | ---------------- | ------------------------------ | ------------------------------------ |
| `GET` | `/global/health` | Get server health and version | `{ healthy: true, version: string }` |
| `GET` | `/global/event` | Get global events (SSE stream) | Event stream |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ---------------- | ----------------------------------- | ------------------------------------ |
| `GET` | `/global/health` | Dohvati zdravlje i verziju servera | `{ healthy: true, version: string }` |
| `GET` | `/global/event` | Dohvati globalne događaje (SSE tok) | Event stream |
---
### Projekt
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------ | ----------------------- | --------------------------------------------- |
| `GET` | `/project` | List all projects | <a href={typesUrl}><code>Project[]</code></a> |
| `GET` | `/project/current` | Get the current project | <a href={typesUrl}><code>Project</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------ | ------------------------ | --------------------------------------------- |
| `GET` | `/project` | Izlistaj sve projekte | <a href={typesUrl}><code>Project[]</code></a> |
| `GET` | `/project/current` | Dohvati trenutni projekt | <a href={typesUrl}><code>Project</code></a> |
---
### Putanja i VCS
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------------------ | ------------------------------------------- |
| `GET` | `/path` | Get the current path | <a href={typesUrl}><code>Path</code></a> |
| `GET` | `/vcs` | Get VCS info for the current project | <a href={typesUrl}><code>VcsInfo</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------------------------- | ------------------------------------------- |
| `GET` | `/path` | Dohvati trenutnu putanju | <a href={typesUrl}><code>Path</code></a> |
| `GET` | `/vcs` | Dohvati VCS informacije za trenutni projekt | <a href={typesUrl}><code>VcsInfo</code></a> |
---
### Instanca
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------- | ---------------------------- | --------- |
| `POST` | `/instance/dispose` | Dispose the current instance | `boolean` |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------- | ----------------------- | --------- |
| `POST` | `/instance/dispose` | Ugasi trenutnu instancu | `boolean` |
---
### Konfiguracija
| Metoda | Putanja | Opis | Odgovor |
| ------- | ------------------- | --------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `PATCH` | `/config` | Update config | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
| Metoda | Putanja | Opis | Odgovor |
| ------- | ------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET` | `/config` | Dohvati informacije o konfiguraciji | <a href={typesUrl}><code>Config</code></a> |
| `PATCH` | `/config` | Ažuriraj konfiguraciju | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | Izlistaj provajdere i zadane modele | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
---
### Provajder
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------- |
| `GET` | `/provider` | List all providers | `{ all: `<a href={typesUrl}>Provider[]</a>`, default: {...}, connected: string[] }` |
| `GET` | `/provider/auth` | Get provider authentication methods | `{ [providerID: string]: `<a href={typesUrl}>ProviderAuthMethod[]</a>` }` |
| `POST` | `/provider/{id}/oauth/authorize` | Authorize a provider using OAuth | <a href={typesUrl}><code>ProviderAuthAuthorization</code></a> |
| `POST` | `/provider/{id}/oauth/callback` | Handle OAuth callback for a provider | `boolean` |
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------- |
| `GET` | `/provider` | Izlistaj sve provajdere | `{ all: `<a href={typesUrl}>Provider[]</a>`, default: {...}, connected: string[] }` |
| `GET` | `/provider/auth` | Dohvati metode autentifikacije provajdera | `{ [providerID: string]: `<a href={typesUrl}>ProviderAuthMethod[]</a>` }` |
| `POST` | `/provider/{id}/oauth/authorize` | Autoriziraj provajdera koristeći OAuth | <a href={typesUrl}><code>ProviderAuthAuthorization</code></a> |
| `POST` | `/provider/{id}/oauth/callback` | Obradi OAuth povratni poziv za provajdera | `boolean` |
---
### Sesije
| Metoda | Putanja | Opis | Napomene |
| -------- | ---------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | List all sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` |
| `GET` | `/session/:id` | Get session details | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Get a session's child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns <a href={typesUrl}><code>Todo[]</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
| `POST` | `/session/:id/share` | Share a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response, remember? }`, returns `boolean` |
| Metoda | Putanja | Opis | Napomene |
| -------- | ---------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | Izlistaj sve sesije | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Kreiraj novu sesiju | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Dohvati status sesije za sve sesije | Returns `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` |
| `GET` | `/session/:id` | Dohvati detalje sesije | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Obriši sesiju i sve njene podatke | Returns `boolean` |
| `PATCH` | `/session/:id` | Ažuriraj svojstva sesije | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Dohvati pod-sesije sesije | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id/todo` | Dohvati listu zadataka za sesiju | Returns <a href={typesUrl}><code>Todo[]</code></a> |
| `POST` | `/session/:id/init` | Analiziraj aplikaciju i kreiraj `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Granaj postojeću sesiju na poruci | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Prekini sesiju u toku | Returns `boolean` |
| `POST` | `/session/:id/share` | Podijeli sesiju | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Prestani dijeliti sesiju | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/diff` | Dohvati razlike za ovu sesiju | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Rezimiraj sesiju | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Vrati poruku | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Vrati sve vraćene poruke | Returns `boolean` |
| `POST` | `/session/:id/permissions/:permissionID` | Odgovori na zahtjev za dozvolu | body: `{ response, remember? }`, returns `boolean` |
---
### Poruke
| Metoda | Putanja | Opis | Napomene |
| ------ | --------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session/:id/message` | List messages in a session | query: `limit?`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `POST` | `/session/:id/message` | Send a message and wait for response | body: `{ messageID?, model?, agent?, noReply?, system?, tools?, parts }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/prompt_async` | Send a message asynchronously (no wait) | body: same as `/session/:id/message`, returns `204 No Content` |
| `POST` | `/session/:id/command` | Execute a slash command | body: `{ messageID?, agent?, model?, command, arguments }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/shell` | Run a shell command | body: `{ agent, model?, command }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| Metoda | Putanja | Opis | Napomene |
| ------ | --------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session/:id/message` | Izlistaj poruke u sesiji | query: `limit?`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `POST` | `/session/:id/message` | Pošalji poruku i čekaj odgovor | body: `{ messageID?, model?, agent?, noReply?, system?, tools?, parts }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `GET` | `/session/:id/message/:messageID` | Dohvati detalje poruke | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/prompt_async` | Pošalji poruku asinkrono (bez čekanja) | body: same as `/session/:id/message`, returns `204 No Content` |
| `POST` | `/session/:id/command` | Izvrši slash naredbu | body: `{ messageID?, agent?, model?, command, arguments }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/shell` | Pokreni shell naredbu | body: `{ agent, model?, command }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
---
### Naredbe
| Metoda | Putanja | Opis | Odgovor |
| ------ | ---------- | ----------------- | --------------------------------------------- |
| `GET` | `/command` | List all commands | <a href={typesUrl}><code>Command[]</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ---------- | -------------------- | --------------------------------------------- |
| `GET` | `/command` | Izlistaj sve naredbe | <a href={typesUrl}><code>Command[]</code></a> |
---
### Datoteke
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files and directories by name | `string[]` (paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | List files and directories | <a href={typesUrl}><code>FileNode[]</code></a> |
| `GET` | `/file/content?path=<p>` | Read a file | <a href={typesUrl}><code>FileContent</code></a> |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Traži tekst u datotekama | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Pronađi datoteke i direktorije po imenu | `string[]` (paths) |
| `GET` | `/find/symbol?query=<q>` | Pronađi simbole radnog prostora | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | Izlistaj datoteke i direktorije | <a href={typesUrl}><code>FileNode[]</code></a> |
| `GET` | `/file/content?path=<p>` | Pročitaj datoteku | <a href={typesUrl}><code>FileContent</code></a> |
| `GET` | `/file/status` | Dohvati status za praćene datoteke | <a href={typesUrl}><code>File[]</code></a> |
#### `/find/file` parametri upita
@@ -209,10 +209,10 @@ opencode server izlaže sljedece API-je.
### Alati (Eksperimentalno)
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------------------------------- | ---------------------------------------- | -------------------------------------------- |
| `GET` | `/experimental/tool/ids` | List all tool IDs | <a href={typesUrl}><code>ToolIDs</code></a> |
| `GET` | `/experimental/tool?provider=<p>&model=<m>` | List tools with JSON schemas for a model | <a href={typesUrl}><code>ToolList</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------------------------------------- | -------------------------------------- | -------------------------------------------- |
| `GET` | `/experimental/tool/ids` | Izlistaj sve ID-ove alata | <a href={typesUrl}><code>ToolIDs</code></a> |
| `GET` | `/experimental/tool?provider=<p>&model=<m>` | Izlistaj alate sa JSON šemama za model | <a href={typesUrl}><code>ToolList</code></a> |
---
@@ -220,65 +220,65 @@ opencode server izlaže sljedece API-je.
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------------ | -------------------------- | -------------------------------------------------------- |
| `GET` | `/lsp` | Get LSP server status | <a href={typesUrl}><code>LSPStatus[]</code></a> |
| `GET` | `/formatter` | Get formatter status | <a href={typesUrl}><code>FormatterStatus[]</code></a> |
| `GET` | `/mcp` | Get MCP server status | `{ [name: string]: `<a href={typesUrl}>MCPStatus</a>` }` |
| `POST` | `/mcp` | Add MCP server dynamically | body: `{ name, config }`, returns MCP status object |
| `GET` | `/lsp` | Dohvati status LSP servera | <a href={typesUrl}><code>LSPStatus[]</code></a> |
| `GET` | `/formatter` | Dohvati status formatera | <a href={typesUrl}><code>FormatterStatus[]</code></a> |
| `GET` | `/mcp` | Dohvati status MCP servera | `{ [name: string]: `<a href={typesUrl}>MCPStatus</a>` }` |
| `POST` | `/mcp` | Dodaj MCP server dinamički | body: `{ name, config }`, returns MCP status object |
---
### Agenti
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------- | ------------------------- | ------------------------------------------- |
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------- | ---------------------------- | ------------------------------------------- |
| `GET` | `/agent` | Izlistaj sve dostupne agente | <a href={typesUrl}><code>Agent[]</code></a> |
---
### Bilježenje
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------------------------------------------ | --------- |
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------------------------------------------------- | --------- |
| `POST` | `/log` | Upiši zapis dnevnika. Tijelo: `{ service, level, message, extra? }` | `boolean` |
---
### TUI
| Metoda | Putanja | Opis | Odgovor |
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ----------------------- | -------------------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Dodaj tekst na prompt | `boolean` |
| `POST` | `/tui/open-help` | Otvori dijalog za pomoć | `boolean` |
| `POST` | `/tui/open-sessions` | Otvori selektor sesija | `boolean` |
| `POST` | `/tui/open-themes` | Otvori selektor tema | `boolean` |
| `POST` | `/tui/open-models` | Otvori selektor modela | `boolean` |
| `POST` | `/tui/submit-prompt` | Pošalji trenutni prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Očisti prompt | `boolean` |
| `POST` | `/tui/execute-command` | Izvrši naredbu (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Prikaži toast obavijest (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Čekaj sljedeći kontrolni zahtjev | Control request object |
| `POST` | `/tui/control/response` | Odgovori na kontrolni zahtjev (`{ body }`) | `boolean` |
---
### Autentifikacija
| Metoda | Putanja | Opis | Odgovor |
| ------ | ----------- | --------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ----------- | ------------------------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Postavi autentifikacijske vjerodajnice. Tijelo mora odgovarati shemi provajdera | `boolean` |
---
### Događaji
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
| Metoda | Putanja | Opis | Odgovor |
| ------ | -------- | -------------------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Tok događaja koje šalje server. Prvi događaj je `server.connected`, zatim bus događaji | Server-sent events stream |
---
### Dokumentacija
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------- | --------------------------- |
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
| Metoda | Putanja | Opis | Odgovor |
| ------ | ------- | ------------------------- | --------------------------------------- |
| `GET` | `/doc` | OpenAPI 3.1 specifikacija | HTML stranica sa OpenAPI specifikacijom |

View File

@@ -61,11 +61,11 @@ System tema je za korisnike koji:
## Korištenje teme
Temu mozete izabrati preko selektora tema komandom `/theme`. Mozete je navesti i u [configu](/docs/config).
Temu mozete izabrati preko selektora tema komandom `/theme`. Ili je možete navesti u `tui.json`.
```json title="opencode.json" {3}
```json title="tui.json" {3}
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"theme": "tokyonight"
}
```

View File

@@ -235,7 +235,7 @@ Podijelite trenutnu sesiju. [Saznajte više](/docs/share).
Navedite dostupne teme.
```bash frame="none"
/theme
/themes
```
**Tastatura:** `ctrl+x t`
@@ -358,24 +358,34 @@ Nekim uređivačima su potrebni argumenti komandne linije da bi se pokrenuli u n
## Konfiguracija
Možete prilagoditi TUI ponašanje putem vašeg OpenCode konfiguracionog fajla.
Možete prilagoditi TUI ponašanje putem `tui.json` (ili `tui.jsonc`).
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"tui": {
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
}
}
"$schema": "https://opencode.ai/tui.json",
"theme": "opencode",
"keybinds": {
"leader": "ctrl+x"
},
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
}
```
Ovo je odvojeno od `opencode.json`, koji konfiguriše ponašanje servera/izvršavanja.
### Opcije
- `scroll_acceleration` - Omogućite ubrzanje pomicanja u macOS stilu za glatko, prirodno pomicanje. Kada je omogućeno, brzina pomicanja se povećava brzim pokretima pomicanja i ostaje precizna za sporije pokrete. **Ova postavka ima prednost nad `scroll_speed` i nadjačava je kada je omogućena.**
- `scroll_speed` - Kontrolira koliko brzo TUI skroluje kada se koriste komande za pomeranje (minimalno: `1`). Podrazumevano je `3`. **Napomena: Ovo se zanemaruje ako je `scroll_acceleration.enabled` postavljeno na `true`.**
- `theme` - Postavlja vašu UI temu. [Saznajte više](/docs/themes).
- `keybinds` - Prilagođava prečice na tastaturi. [Saznajte više](/docs/keybinds).
- `scroll_acceleration.enabled` - Omogućite ubrzanje pomicanja u macOS stilu za glatko, prirodno pomicanje. Kada je omogućeno, brzina pomicanja se povećava brzim pokretima pomicanja i ostaje precizna za sporije pokrete. **Ova postavka ima prednost nad `scroll_speed` i nadjačava je kada je omogućena.**
- `scroll_speed` - Kontrolira koliko brzo TUI skroluje kada se koriste komande za pomeranje (minimum: `0.001`, podržava decimalne vrijednosti). Podrazumevano je `3`. **Napomena: Ovo se zanemaruje ako je `scroll_acceleration.enabled` postavljeno na `true`.**
- `diff_style` - Kontrolira prikazivanje razlike. `"auto"` se prilagođava širini terminala, `"stacked"` uvijek prikazuje raspored u jednoj koloni.
Koristite `OPENCODE_TUI_CONFIG` da učitate prilagođenu putanju TUI konfiguracije.
---

View File

@@ -3,7 +3,7 @@ title: Zen
description: Kurirana lista modela koje nudi OpenCode.
---
import config from "../../../../config.mjs"
import config from "../../../config.mjs"
export const console = config.console
export const email = `mailto:${config.email}`
@@ -55,6 +55,7 @@ Nasim modelima mozete pristupiti i preko sljedecih API endpointa.
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -64,22 +65,24 @@ Nasim modelima mozete pristupiti i preko sljedecih API endpointa.
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 Free | minimax-m2.1-free | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 Free | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -106,29 +109,34 @@ Podrzavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
| Model | Input | Output | Cached Read | Cached Write |
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.1 Free | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 4.7 Free | Free | Free | Free | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 Free | Free | Free | Free | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.1 | $1.07 | $8.50 | $0.107 | - |
@@ -147,10 +155,8 @@ Naknade kartica se prenose po stvarnom trosku (4.4% + $0.30 po transakciji) i ne
Besplatni modeli:
- GLM 4.7 Free je dostupan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje feedbacka i unapredenje modela.
- Kimi K2.5 Free je dostupan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje feedbacka i unapredenje modela.
- MiniMax M2.1 Free je dostupan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje feedbacka i unapredenje modela.
- Big Pickle je stealth model koji je besplatan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje feedbacka i unapredenje modela.
- MiniMax M2.5 Free je dostupan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje povratnih informacija i poboljsanje modela.
- Big Pickle je stealth model koji je besplatan na OpenCode ograniceno vrijeme. Tim koristi taj period za prikupljanje povratnih informacija i poboljsanje modela.
Ako imate pitanja, <a href={email}>kontaktirajte nas</a>.
@@ -177,11 +183,9 @@ Na primjer, ako postavite mjesecni limit na $20, Zen nece potrositi vise od $20
Svi nasi modeli su hostovani u SAD-u. Provajderi prate zero-retention politiku i ne koriste vase podatke za treniranje modela, uz sljedece izuzetke:
- Big Pickle: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljsanje modela.
- GLM 4.7 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljsanje modela.
- Kimi K2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljsanje modela.
- MiniMax M2.1 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljsanje modela.
- OpenAI API-ji: Zahtjevi se cuvaju 30 dana prema [OpenAI Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic API-ji: Zahtjevi se cuvaju 30 dana prema [Anthropic Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
- MiniMax M2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljsanje modela.
- OpenAI API-ji: Zahtjevi se cuvaju 30 dana prema [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic API-ji: Zahtjevi se cuvaju 30 dana prema [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
---

View File

@@ -558,6 +558,7 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler.
| `OPENCODE_AUTO_SHARE` | boolean | Del automatisk session |
| `OPENCODE_GIT_BASH_PATH` | string | Sti til Git Bash eksekverbar på Windows |
| `OPENCODE_CONFIG` | string | Sti til konfigurationsfil |
| `OPENCODE_TUI_CONFIG` | string | Sti til TUI-konfigurationsfil |
| `OPENCODE_CONFIG_DIR` | string | Sti til konfigurationsmappe |
| `OPENCODE_CONFIG_CONTENT` | string | Indbygget json-konfigurationsindhold |
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Deaktiver automatisk opdateringskontrol |
@@ -582,7 +583,7 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler.
---
### Flag
### Eksperimentel
Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes.

View File

@@ -14,10 +14,11 @@ OpenCode understøtter både **JSON** og **JSONC** (JSON med kommentarer) format
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
// Theme configuration
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-5",
"autoupdate": true,
"server": {
"port": 4096,
},
}
```
@@ -34,7 +35,7 @@ Konfigurationsfiler **flettes sammen**, erstattes ikke.
Konfigurationsfiler flettes sammen, erstattes ikke. Indstillinger fra følgende konfigurationssteder kombineret. Senere konfigurationer tilsidesætter kun tidligere konfigurationer for modstridende nøgler. Ikke-modstridende indstillinger fra alle konfigurationer bevares.
For eksempel, hvis dine globale konfigurationssæt `theme: "opencode"` og `autoupdate: true`, og dine projektkonfigurationssæt `model: "anthropic/claude-sonnet-4-5"`, vil den endelige konfiguration integrere alle tre indstillinger.
For eksempel, hvis dine globale konfigurationssæt `autoupdate: true`, og dine projektkonfigurationssæt `model: "anthropic/claude-sonnet-4-5"`, vil den endelige konfiguration integrere begge indstillinger.
---
@@ -490,13 +491,15 @@ Du kan styre kontekstkomprimeringsadfærd gennem indstillingen `compaction`.
"$schema": "https://opencode.ai/config.json",
"compaction": {
"auto": true,
"prune": true
"prune": true,
"reserved": 10000
}
}
```
- `auto` - Komprimer automatisk sessionen, når konteksten er fuld (standard: `true`).
- `prune` - Fjern gamle værktøjsudgange for at gemme tokens (standard: `true`).
- `reserved` - Tokenbuffer til komprimering. Efterlader nok vindue til at undgå overløb under komprimering
---
@@ -582,8 +585,8 @@ Du kan deaktivere udbydere, der indlæses automatisk gennem `disabled_providers`
Indstillingen `disabled_providers` accepterer en række udbyder-id'er. Når en udbyder er deaktiveret:
- Det vil ikke blive indlæst, omgivelserne miljøvariabler er indstillet.
- Den vil ikke blive indlæst, gennem API-nøgler er konfigureret kommandoen `/connect`.
- Den vil ikke blive indlæst, selvom miljøvariabler er indstillet.
- Den vil ikke blive indlæst, selvom API-nøgler er konfigureret via `/connect`-kommandoen.
- Udbyderens modeller vises ikke på modelvalgslisten.
---

View File

@@ -79,6 +79,32 @@ Dette skaber værktøjer: `math_add` og `math_multiply`.
---
#### Navnekollisioner med indbyggede værktøjer
Brugerdefinerede værktøjer er identificeret ved værktøjsnavn. Hvis et brugerdefineret værktøj bruger samme navn som et indbygget værktøj, har det brugerdefinerede værktøj forrang.
For eksempel erstatter denne fil det indbyggede `bash` værktøj:
```ts title=".opencode/tools/bash.ts"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Restricted bash wrapper",
args: {
command: tool.schema.string(),
},
async execute(args) {
return `blocked: ${args.command}`
},
})
```
:::note
Foretræk unikke navne, medmindre du bevidst ønsker at erstatte et indbygget værktøj. Hvis du vil deaktivere et indbygget værktøj, men ikke overskrive det, skal du bruge [tilladelser](/docs/permissions).
:::
---
### Argumenter
Du kan bruge `tool.schema`, som kun er [Zod](https://zod.dev), til at definere argumenttyper.

View File

@@ -3,11 +3,11 @@ title: Tastebindinger
description: Tilpas dine nøglebindinger.
---
OpenCode har en liste over nøglebindinger, som du kan tilpasse gennem OpenCode-konfigurationen.
OpenCode har en liste over nøglebindinger, som du kan tilpasse gennem `tui.json`.
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"leader": "ctrl+x",
"app_exit": "ctrl+c,ctrl+d,<leader>q",
@@ -117,11 +117,11 @@ Du behøver ikke bruge en ledernøgle til dine nøglebindinger, men vi anbefaler
## Deaktiver tastebinding
Du kan deaktivere en nøglebinding ved at tilføje nøglen til din konfiguration med værdien "ingen".
Du kan deaktivere en nøglebinding ved at tilføje nøglen til `tui.json` med værdien "none".
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"session_compact": "none"
}

View File

@@ -20,13 +20,14 @@ OpenCode leveres med flere indbyggede LSP-servere til populære sprog:
| clojure-lsp | .clj,.cljs,.cljc,.edn | `clojure-lsp` kommando tilgængelig |
| dart | .dart | `dart` kommando tilgængelig |
| deno | .ts,.tsx,.js,.jsx,.mjs | `deno` kommando tilgængelig (auto-detects deno.json/deno.jsonc) |
| eliksir-ls | .ex,.exs | `elixir` kommando tilgængelig |
| elixir-ls | .ex,.exs | `elixir` kommando tilgængelig |
| eslint | .ts,.tsx,.js,.jsx,.mjs,.cjs,.mts,.cts,.vue | `eslint` afhængighed i projekt |
| fsharp | .fs,.fsi,.fsx,.fsscript | `.NET SDK` installere |
| gleam | .gleam | `gleam` kommando tilgængelig |
| gopls | .go | `go` kommando tilgængelig |
| hls | .hs,.lhs | `haskell-language-server-wrapper` kommando tilgængelig |
| jdtls | .java | `Java SDK (version 21+)` installere |
| julials | .jl | `julia` og `LanguageServer.jl` installeret |
| kotlin-ls | .kt,.kts | Autoinstallationer til Kotlin-projekter |
| lua-ls | .lua | Autoinstallationer til Lua-projekter |
| nixd | .nix | `nixd` kommando tilgængelig |
@@ -46,7 +47,7 @@ OpenCode leveres med flere indbyggede LSP-servere til populære sprog:
| yaml-ls | .yaml,.yml | Autoinstallerer Red Hat yaml-language-server |
| zls | .zig,.zon | `zig` kommando tilgængelig |
LSP-servere aktiveres automatisk, når en af ovnstående filtypenavne opdages, og kravene er opfyldt.
LSP-servere aktiveres automatisk, når en af ovenstående filtypenavne opdages, og kravene er opfyldt.
:::note
Du kan deaktivere automatisk LSP-serverdownloads ved at indstille miljøvariablen `OPENCODE_DISABLE_LSP_DOWNLOAD` til `true`.

View File

@@ -119,7 +119,7 @@ Plugin-funktionen modtager:
- `directory`: Den aktuelle arbejdsmappe.
- `worktree`: Git worktree-stien.
- `client`: En opencode SDK klient til interaktion med AI.
- `-: Buns [shell API](https://bun.com/docs/runtime/shell) til udførelse af kommandoer.
- `$`: Buns [shell API](https://bun.com/docs/runtime/shell) til udførelse af kommandoer.
---
@@ -308,6 +308,10 @@ export const CustomToolsPlugin: Plugin = async (ctx) => {
Dine tilpassede værktøjer vil være tilgængelige for opencode sammen med indbyggede værktøjer.
:::note
Hvis et plugin-værktøj bruger samme navn som et indbygget værktøj, har plugin-værktøjet forrang.
:::
---
### Logning

View File

@@ -81,6 +81,37 @@ Det fungerer som alle andre udbydere i OpenCode og er helt valgfrit at bruge.
---
## OpenCode Go
OpenCode Go er en billig abonnementsplan, der giver pålidelig adgang til populære åbne kodningsmodeller leveret af OpenCode-teamet, som er testet og verificeret til at fungere godt med OpenCode.
1. Kør kommandoen `/connect` i TUI, vælg `OpenCode Go`, og gå til [opencode.ai/auth](https://opencode.ai/zen).
```txt
/connect
```
2. Log ind, tilføj dine faktureringsoplysninger og kopier din API-nøgle.
3. Indsæt din API-nøgle.
```txt
┌ API key
└ enter
```
4. Kør `/models` i TUI for at se listen over modeller, vi anbefaler.
```txt
/models
```
Det fungerer som alle andre udbydere i OpenCode og er helt valgfrit at bruge.
---
## Katalog
Lad os se på nogle af udbyderne i detaljer. Hvis du vil tilføje en udbyder til listen, er du velkommen til at åbne en PR.
@@ -1474,6 +1505,39 @@ SAP AI Core giver adgang til 40+ modeller fra OpenAI, Anthropic, Google, Amazon,
---
### STACKIT
STACKIT AI Model Serving leverer fuldt administreret suverænt hostingmiljø til AI-modeller, med fokus på LLM'er som Llama, Mistral og Qwen, med maksimal datasuverænitet på europæisk infrastruktur.
1. Gå til [STACKIT Portal](https://portal.stackit.cloud), naviger til **AI Model Serving**, og opret en auth-token til dit projekt.
:::tip
Du skal have en STACKIT-kundekonto, brugerkonto og projekt, før du opretter auth-tokens.
:::
2. Kør kommandoen `/connect` og søg efter **STACKIT**.
```txt
/connect
```
3. Indtast din STACKIT AI Model Serving auth-token.
```txt
┌ API key
└ enter
```
4. Kør kommandoen `/models` for at vælge fra tilgængelige modeller som _Qwen3-VL 235B_ eller _Llama 3.3 70B_.
```txt
/models
```
---
### OVHcloud AI Endpoints
1. Gå til [OVHcloud-panelet](https://ovh.com/manager). Naviger til `Public Cloud`-delen, `AI & Machine Learning` > `AI Endpoints` og i `API Keys`-fanen klikker du på **Opret en ny API-nøgle**.

View File

@@ -117,6 +117,78 @@ try {
---
## Struktureret output
Du kan anmode om struktureret JSON-output fra modellen ved at angive et `format` med et JSON-skema. Modellen vil bruge et `StructuredOutput`-værktøj til at returnere valideret JSON, der matcher dit skema.
### Grundlæggende brug
```typescript
const result = await client.session.prompt({
path: { id: sessionId },
body: {
parts: [{ type: "text", text: "Research Anthropic and provide company info" }],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: { type: "string", description: "Company name" },
founded: { type: "number", description: "Year founded" },
products: {
type: "array",
items: { type: "string" },
description: "Main products",
},
},
required: ["company", "founded"],
},
},
},
})
// Access the structured output
console.log(result.data.info.structured_output)
// { company: "Anthropic", founded: 2021, products: ["Claude", "Claude API"] }
```
### Outputformat-typer
| Type | Beskrivelse |
| ------------- | -------------------------------------------------------- |
| `text` | Standard. Standard tekstsvar (intet struktureret output) |
| `json_schema` | Returnerer valideret JSON, der matcher det angivne skema |
### JSON-skemaformat
Når du bruger `type: 'json_schema'`, skal du angive:
| Felt | Type | Beskrivelse |
| ------------ | --------------- | ---------------------------------------------------------- |
| `type` | `'json_schema'` | Påkrævet. Angiver JSON-skematilstand |
| `schema` | `object` | Påkrævet. JSON-skemaobjekt, der definerer outputstrukturen |
| `retryCount` | `number` | Valgfri. Antal valideringsforsøg (standard: 2) |
### Fejlhåndtering
Hvis modellen ikke formår at producere gyldigt struktureret output efter alle forsøg, vil svaret inkludere en `StructuredOutputError`:
```typescript
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Failed to produce structured output:", result.data.info.error.message)
console.error("Attempts:", result.data.info.error.retries)
}
```
### Bedste praksis
1. **Giv klare beskrivelser** i dine skemaegenskaber for at hjælpe modellen med at forstå, hvilke data der skal udtrækkes
2. **Brug `required`** til at angive, hvilke felter der skal være til stede
3. **Hold skemaer fokuserede** - komplekse indlejrede skemaer kan være sværere for modellen at udfylde korrekt
4. **Indstil passende `retryCount`** - øg for komplekse skemaer, sænk for enkle
---
## API'er
SDK avslører alle server-APIer gjennom en typesikker klient.
@@ -226,27 +298,27 @@ const { providers, default: defaults } = await client.config.providers()
### Sessioner
| Metode | Beskrivelse | Noter |
| ---------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `session.list()` | Liste sessioner | Returnerer <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Få session | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | Liste over barnesessioner | Returnerer <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Opret session | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Slett session | Returnerer `boolean` |
| `session.update({ path, body })` | Opdater sessionegenskaper | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyser appen og lag `AGENTS.md` | Returnerer `boolean` |
| `session.abort({ path })` | Avbryt en løpesession | Returnerer `boolean` |
| `session.share({ path })` | Del sessionen | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Slutt at dele sessionen | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Oppsummer sessionen | Returnerer `boolean` |
| `session.messages({ path })` | Liste meldinger i en session | Returnerer `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Få meldingsdetaljer | Returnerer `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send melding | `body.noReply: true` returnerer UserMessage (kun kontekst). Standard returnerer <a href={typesUrl}><code>AssistantMessage</code></a> med AI svar |
| `session.command({ path, body })` | Send kommando til session | Returnerer `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Kjør en shell-kommando | Returnerer <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Tilbakestill en melding | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Gjenopret nulstillete meldinger | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Svar på en tillatelsesforespørsel | Returnerer `boolean` |
| Metode | Beskrivelse | Noter |
| ---------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | Liste sessioner | Returnerer <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Få session | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | Liste over barnesessioner | Returnerer <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Opret session | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Slett session | Returnerer `boolean` |
| `session.update({ path, body })` | Opdater sessionegenskaper | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyser appen og lag `AGENTS.md` | Returnerer `boolean` |
| `session.abort({ path })` | Avbryt en løpesession | Returnerer `boolean` |
| `session.share({ path })` | Del sessionen | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Slutt at dele sessionen | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Oppsummer sessionen | Returnerer `boolean` |
| `session.messages({ path })` | Liste meldinger i en session | Returnerer `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Få meldingsdetaljer | Returnerer `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send melding | `body.noReply: true` returnerer UserMessage (kun kontekst). Standard returnerer <a href={typesUrl}><code>AssistantMessage</code></a> med AI svar. Understøtter `body.outputFormat` for [struktureret output](#struktureret-output) |
| `session.command({ path, body })` | Send kommando til session | Returnerer `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Kjør en shell-kommando | Returnerer <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Tilbakestill en melding | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Gjenopret nulstillete meldinger | Returnerer <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Svar på en tillatelsesforespørsel | Returnerer `boolean` |
---

View File

@@ -61,11 +61,11 @@ Systemtemaet er for brugere som:
## Brug et tema
Du kan velge et tema ved at hente frem temavalg med kommandoen `/theme`. Eller du kan spesifisere det i [config](/docs/config).
Du kan velge et tema ved at hente frem temavalg med kommandoen `/theme`. Eller du kan angive det i `tui.json`.
```json title="opencode.json" {3}
```json title="tui.json" {3}
{
"$schema": "https://opencode.ai/config.json",
"$schema": "https://opencode.ai/tui.json",
"theme": "tokyonight"
}
```

View File

@@ -352,24 +352,34 @@ Nogle editorer kræver kommandolinjeargumenter for at køre i blokeringstilstand
## Konfigurer
Du kan tilpasse TUI-adfærden gennem OpenCode-konfigurationsfilen.
Du kan tilpasse TUI-adfærd gennem `tui.json` (eller `tui.jsonc`).
```json title="opencode.json"
```json title="tui.json"
{
"$schema": "https://opencode.ai/config.json",
"tui": {
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
}
}
"$schema": "https://opencode.ai/tui.json",
"theme": "opencode",
"keybinds": {
"leader": "ctrl+x"
},
"scroll_speed": 3,
"scroll_acceleration": {
"enabled": true
},
"diff_style": "auto"
}
```
Dette er adskilt fra `opencode.json`, som konfigurerer server-/kørselstidsadfærd.
### Indstillinger
- `scroll_acceleration` - Aktiver rulleacceleration i macOS-stil for jævn, naturlig rulning. Når aktiveret, øger rullehastigheden med hurtige rullebevægelser og forbliver præcis for langsommere bevægelser. **Denne indstilling har forrang over `scroll_speed` og tilsidesætter den, når den er aktiveret.**
- `scroll_speed` - Styrer hvor hurtigt TUI ruller, når du bruger rullekommandoer (minimum: `1`). Standard er `3`. **Bemærk: Dette ignoreres hvis `scroll_acceleration.enabled` er sat til `true`.**
- `theme` - Indstiller dit brugergrænsefladetema. [Læs mere](/docs/themes).
- `keybinds` - Tilpasser tastaturgenveje. [Læs mere](/docs/keybinds).
- `scroll_acceleration.enabled` - Aktiver rulleacceleration i macOS-stil for jævn, naturlig rulning. Når aktiveret, øger rullehastigheden med hurtige rullebevægelser og forbliver præcis for langsommere bevægelser. **Denne indstilling har forrang over `scroll_speed` og tilsidesætter den, når den er aktiveret.**
- `scroll_speed` - Styrer hvor hurtigt TUI ruller, når du bruger rullekommandoer (minimum: `0.001`, understøtter decimalværdier). Standard er `3`. **Bemærk: Dette ignoreres hvis `scroll_acceleration.enabled` er sat til `true`.**
- `diff_style` - Styrer diff-gengivelse. `"auto"` tilpasser sig terminalbredde, `"stacked"` viser altid et enkeltkolonne-layout.
Brug `OPENCODE_TUI_CONFIG` til at indlæse en brugerdefineret TUI-konfigurationssti.
---

View File

@@ -64,6 +64,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
| Model | Model ID | Endpoint | AI SDK Pakke |
| ------------------- | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -73,22 +74,24 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Sonnet 4.5 | claude-sonnett-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnett-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.5 Gratis | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 Gratis | minimax-m2.1-free | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 Gratis | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 Gratis | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Tenker | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3-koder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -117,29 +120,34 @@ Vi støtter en pay-as-you-go-model. Nedenfor er priserne **per 1 million tokens*
| Model | Input | Output | Cached Læs | Cached Skriv |
| --------------------------------- | ------ | ------ | ---------- | ------------ |
| Stor sylteagurk | Gratis | Gratis | Gratis | - |
| MiniMax M2.1 Gratis | Gratis | Gratis | Gratis | - |
| MiniMax M2.5 Gratis | Gratis | Gratis | Gratis | - |
| MiniMax M2.5 | $0,30 | $1,20 | $0,06 | - |
| MiniMax M2.1 | $0,30 | $1,20 | $0,10 | - |
| GLM 4.7 Gratis | Gratis | Gratis | Gratis | - |
| GLM 5 | $1,00 | $3,20 | $0,20 | - |
| GLM 4.7 | $0,60 | $2,20 | $0,10 | - |
| GLM 4.6 | $0,60 | $2,20 | $0,10 | - |
| Kimi K2.5 Gratis | Gratis | Gratis | Gratis | - |
| Kimi K2.5 | $0,60 | $3,00 | $0,08 | - |
| Kimi K2 Tenker | $0,40 | $2,50 | - | - |
| Kimi K2 | $0,40 | $2,50 | - | - |
| Qwen3-koder 480B | $0,45 | $1,50 | - | - |
| Claude Opus 4.6 (≤ 200K tokens) | $5,00 | $25,00 | $0,50 | $6,25 |
| Claude Opus 4.6 (> 200K tokens) | $10,00 | $37,50 | $1,00 | $12,50 |
| Claude Opus 4.5 | $5,00 | $25,00 | $0,50 | $6,25 |
| Claude Opus 4.1 | $15,00 | $75,00 | $1,50 | $18,75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3,00 | $15,00 | $0,30 | $3,75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6,00 | $22,50 | $0,60 | $7,50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3,00 | $15,00 | $0,30 | $3,75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6,00 | $22,50 | $0,60 | $7,50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3,00 | $15,00 | $0,30 | $3,75 |
| Claude Sonnet 4 (> 200K tokens) | $6,00 | $22,50 | $0,60 | $7,50 |
| Claude Haiku 4.5 | $1,00 | $5,00 | $0,10 | $1,25 |
| Claude Haiku 3.5 | $0,80 | $4,00 | $0,08 | $1,00 |
| Claude Opus 4.6 (≤ 200K tokens) | $5,00 | $25,00 | $0,50 | $6,25 |
| Claude Opus 4.6 (> 200K tokens) | $10,00 | $37,50 | $1,00 | $12,50 |
| Claude Opus 4.5 | $5,00 | $25,00 | $0,50 | $6,25 |
| Claude Opus 4.1 | $15,00 | $75,00 | $1,50 | $18,75 |
| Gemini 3.1 Pro (≤ 200K tokens) | $2,00 | $12,00 | $0,20 | - |
| Gemini 3.1 Pro (> 200K tokens) | $4,00 | $18,00 | $0,40 | - |
| Gemini 3 Pro (≤ 200K tokens) | $2,00 | $12,00 | $0,20 | - |
| Gemini 3 Pro (> 200K tokens) | $4,00 | $18,00 | $0,40 | - |
| Gemini 3 Flash | $0,50 | $3,00 | $0,05 | - |
| GPT 5.3 Codex | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 Codex | $1,75 | $14,00 | $0,175 | - |
| GPT 5.1 | $1,07 | $8,50 | $0,107 | - |
@@ -158,9 +166,7 @@ Kreditkortgebyrer overføres til kostpris (4,4 % + $0,30 per transaktion); vi op
De gratis modeller:
- GLM 4.7 Gratis er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at samle feedback og forbedre modellen.
- Kimi K2.5 Gratis er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at samle feedback og forbedre modellen.
- MiniMax M2.1 Gratis er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at samle feedback og forbedre modellen.
- MiniMax M2.5 Gratis er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at samle feedback og forbedre modellen.
- Stor sylteagurk er en stealth-model som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at samle feedback og forbedre modellen.
<a href={email}>Kontakt os</a> hvis du har spørgsmål.
@@ -191,9 +197,7 @@ at opkræve dig mere end $20, hvis din saldo går under $5.
Alle vores modeller er hostet i USA. Vores udbydere følger en nul-opbevaringspolitik og bruger ikke dine data til modeltræning, med følgende undtagelser:
- Stor sylteagurk: I løbet af gratisperioden kan indsamlede data bruges til at forbedre modellen.
- GLM 4.7 Gratis: I løbet af gratisperioden kan indsamlede data bruges til at forbedre modellen.
- Kimi K2.5 Gratis: I løbet af gratisperioden kan indsamlede data bruges til at forbedre modellen.
- MiniMax M2.1 Gratis: I løbet af gratisperioden kan indsamlede data bruges til at forbedre modellen.
- MiniMax M2.5 Gratis: I løbet af gratisperioden kan indsamlede data bruges til at forbedre modellen.
- OpenAI API'er: Anmodninger opbevares i 30 dage i overensstemmelse med [OpenAIs datapolitikker](https://platform.openai.com/docs/guides/your-data).
- Anthropic API'er: Anmodninger opbevares i 30 dage i overensstemmelse med [Anthropics datapolitikker](https://docs.anthropic.com/en/docs/claude-code/data-usage).

View File

@@ -84,6 +84,37 @@ Es funktioniert wie jeder andere Anbieter in OpenCode und ist völlig optional.
---
## OpenCode Go
OpenCode Go ist ein kostenguenstiges Abonnement, das zuverlaessigen Zugriff auf beliebte Open-Coding-Modelle bietet, die vom OpenCode-Team getestet und verifiziert wurden, dass sie gut mit OpenCode funktionieren.
1. Führen Sie den Befehl `/connect` in der TUI aus, waehlen Sie `OpenCode Go` und gehen Sie zu [opencode.ai/auth](https://opencode.ai/zen).
```txt
/connect
```
2. Melden Sie sich an, geben Sie Ihre Rechnungsdaten ein und kopieren Sie Ihren API-Schlüssel.
3. Fügen Sie Ihren API-Schlüssel ein.
```txt
┌ API key
└ enter
```
4. Führen Sie `/models` in der TUI aus, um die Liste der empfohlenen Modelle zu sehen.
```txt
/models
```
Es funktioniert wie jeder andere Anbieter in OpenCode und ist völlig optional.
---
## Verzeichnis
Schauen wir uns einige der Anbieter im Detail an. Wenn Sie einen Anbieter hinzufügen möchten
@@ -1480,6 +1511,39 @@ SAP AI Core bietet Zugriff auf 40+ Modelle von OpenAI, Anthropic, Google, Amazon
---
### STACKIT
STACKIT AI Model Serving bietet eine voll verwaltete, souveraene Hosting-Umgebung fuer AI-Modelle, mit Fokus auf LLMs wie Llama, Mistral und Qwen, mit maximaler Datensouveraenitaet auf europaeischer Infrastruktur.
1. Gehen Sie zum [STACKIT Portal](https://portal.stackit.cloud), navigieren Sie zu **AI Model Serving** und erstellen Sie ein Auth-Token fuer Ihr Projekt.
:::tip
Sie benoetigen ein STACKIT-Kundenkonto, Benutzerkonto und Projekt, bevor Sie Auth-Tokens erstellen koennen.
:::
2. Führen Sie den Befehl `/connect` aus und suchen Sie nach **STACKIT**.
```txt
/connect
```
3. Geben Sie Ihr STACKIT AI Model Serving Auth-Token ein.
```txt
┌ API key
└ enter
```
4. Führen Sie den Befehl `/models` aus, um aus verfügbaren Modellen wie _Qwen3-VL 235B_ oder _Llama 3.3 70B_ auszuwählen.
```txt
/models
```
---
### OVHcloud AI Endpoints
1. Gehen Sie zum [OVHcloud panel](https://ovh.com/manager). Navigieren Sie zum Abschnitt `Public Cloud`, `AI & Machine Learning` > `AI Endpoints` und klicken Sie auf der Registerkarte `API Keys` auf **Neuen API-Schlüssel erstellen**.

View File

@@ -119,6 +119,78 @@ try {
---
## Structured Output
Du kannst eine strukturierte JSON-Ausgabe vom Modell anfordern, indem du ein `format` mit einem JSON-Schema angibst. Das Modell verwendet dann ein `StructuredOutput`-Tool, um validiertes JSON zurueckzugeben, das deinem Schema entspricht.
### Grundlegende Verwendung
```typescript
const result = await client.session.prompt({
path: { id: sessionId },
body: {
parts: [{ type: "text", text: "Recherchiere Anthropic und gib Firmeninfos zurueck" }],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: { type: "string", description: "Firmenname" },
founded: { type: "number", description: "Gruendungsjahr" },
products: {
type: "array",
items: { type: "string" },
description: "Hauptprodukte",
},
},
required: ["company", "founded"],
},
},
},
})
// Zugriff auf die strukturierte Ausgabe
console.log(result.data.info.structured_output)
// { company: "Anthropic", founded: 2021, products: ["Claude", "Claude API"] }
```
### Ausgabeformate
| Type | Description |
| ------------- | ----------------------------------------------------------- |
| `text` | Standard. Normale Textantwort (keine strukturierte Ausgabe) |
| `json_schema` | Gibt validiertes JSON zurueck, das dem Schema entspricht |
### JSON-Schema-Format
Bei Verwendung von `type: 'json_schema'` musst du Folgendes angeben:
| Field | Type | Description |
| ------------ | --------------- | ------------------------------------------------------------- |
| `type` | `'json_schema'` | Erforderlich. Gibt den JSON-Schema-Modus an |
| `schema` | `object` | Erforderlich. JSON-Schema-Objekt, das die Struktur definiert |
| `retryCount` | `number` | Optional. Anzahl der Validierungswiederholungen (Standard: 2) |
### Fehlerbehandlung
Wenn das Modell nach allen Wiederholungen keine valide strukturierte Ausgabe liefert, enthaelt die Antwort einen `StructuredOutputError`:
```typescript
if (result.data.info.error?.name === "StructuredOutputError") {
console.error("Strukturierte Ausgabe fehlgeschlagen:", result.data.info.error.message)
console.error("Versuche:", result.data.info.error.retries)
}
```
### Best Practices
1. **Klare Beschreibungen**: Gib in deinen Schema-Properties klare Beschreibungen an, damit das Modell versteht, welche Daten extrahiert werden sollen.
2. **`required` nutzen**: Definiere, welche Felder zwingend vorhanden sein muessen.
3. **Schemas einfach halten**: Komplexe verschachtelte Schemas sind fuer das Modell schwerer korrekt auszufuellen.
4. **`retryCount` anpassen**: Erhoehe den Wert bei komplexen Schemas, verringere ihn bei einfachen.
---
## APIs
Das SDK stellt alle Server-APIs ueber einen typsicheren Client bereit.
@@ -127,9 +199,9 @@ Das SDK stellt alle Server-APIs ueber einen typsicheren Client bereit.
### Global
| Method | Description | Response |
| ----------------- | ------------------------------- | ------------------------------------ |
| `global.health()` | Check server health and version | `{ healthy: true, version: string }` |
| Method | Description | Response |
| ----------------- | -------------------------------- | ------------------------------------ |
| `global.health()` | Prueft Server-Status und Version | `{ healthy: true, version: string }` |
---
@@ -144,10 +216,10 @@ console.log(health.data.version)
### App
| Method | Description | Response |
| -------------- | ------------------------- | ------------------------------------------- |
| `app.log()` | Write a log entry | `boolean` |
| `app.agents()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
| Method | Description | Response |
| -------------- | -------------------------------- | ------------------------------------------- |
| `app.log()` | Schreibt einen Log-Eintrag | `boolean` |
| `app.agents()` | Listet alle verfuegbaren Agenten | <a href={typesUrl}><code>Agent[]</code></a> |
---
@@ -171,10 +243,10 @@ const agents = await client.app.agents()
### Project
| Method | Description | Response |
| ------------------- | ------------------- | --------------------------------------------- |
| `project.list()` | List all projects | <a href={typesUrl}><code>Project[]</code></a> |
| `project.current()` | Get current project | <a href={typesUrl}><code>Project</code></a> |
| Method | Description | Response |
| ------------------- | ---------------------------- | --------------------------------------------- |
| `project.list()` | Listet alle Projekte | <a href={typesUrl}><code>Project[]</code></a> |
| `project.current()` | Ruft das aktuelle Projekt ab | <a href={typesUrl}><code>Project</code></a> |
---
@@ -192,9 +264,9 @@ const currentProject = await client.project.current()
### Path
| Method | Description | Response |
| ------------ | ---------------- | ---------------------------------------- |
| `path.get()` | Get current path | <a href={typesUrl}><code>Path</code></a> |
| Method | Description | Response |
| ------------ | -------------------------- | ---------------------------------------- |
| `path.get()` | Ruft den aktuellen Pfad ab | <a href={typesUrl}><code>Path</code></a> |
---
@@ -209,10 +281,10 @@ const pathInfo = await client.path.get()
### Config
| Method | Description | Response |
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `config.get()` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `config.providers()` | List providers and default models | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
| Method | Description | Response |
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `config.get()` | Ruft Konfigurationsinfos ab | <a href={typesUrl}><code>Config</code></a> |
| `config.providers()` | Listet Provider und Standard-Modelle | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
---
@@ -228,27 +300,27 @@ const { providers, default: defaults } = await client.config.providers()
### Sessions
| Method | Description | Notes |
| ---------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Delete session | Returns `boolean` |
| `session.update({ path, body })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ path })` | Abort a running session | Returns `boolean` |
| `session.share({ path })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
| `session.messages({ path })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns <a href={typesUrl}><code>AssistantMessage</code></a> with AI response |
| `session.command({ path, body })` | Send command to session | Returns `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
| Method | Description | Notes |
| ---------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | Listet Sessions | Gibt <a href={typesUrl}><code>Session[]</code></a> zurueck |
| `session.get({ path })` | Ruft Session ab | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.children({ path })` | Listet Kind-Sessions | Gibt <a href={typesUrl}><code>Session[]</code></a> zurueck |
| `session.create({ body })` | Erstellt Session | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.delete({ path })` | Loescht Session | Gibt `boolean` zurueck |
| `session.update({ path, body })` | Aktualisiert Session-Eigenschaften | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.init({ path, body })` | Analysiert App und erstellt `AGENTS.md` | Gibt `boolean` zurueck |
| `session.abort({ path })` | Bricht eine laufende Session ab | Gibt `boolean` zurueck |
| `session.share({ path })` | Teilt Session | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.unshare({ path })` | Hebt Teilen der Session auf | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.summarize({ path, body })` | Fasst Session zusammen | Gibt `boolean` zurueck |
| `session.messages({ path })` | Listet Nachrichten einer Session | Gibt `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` zurueck |
| `session.message({ path })` | Ruft Nachrichtendetails ab | Gibt `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` zurueck |
| `session.prompt({ path, body })` | Sendet Prompt-Nachricht | `body.noReply: true` gibt UserMessage (nur Kontext) zurueck. Standard gibt <a href={typesUrl}><code>AssistantMessage</code></a> mit AI-Antwort zurueck. Unterstuetzt `body.outputFormat` fuer [strukturierte Ausgabe](#structured-output) |
| `session.command({ path, body })` | Sendet Befehl an Session | Gibt `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` zurueck |
| `session.shell({ path, body })` | Fuehrt Shell-Befehl aus | Gibt <a href={typesUrl}><code>AssistantMessage</code></a> zurueck |
| `session.revert({ path, body })` | Setzt Nachricht zurueck | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `session.unrevert({ path })` | Stellt zurueckgesetzte Nachrichten wieder her | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Antwortet auf eine Berechtigungsanfrage | Gibt `boolean` zurueck |
---
@@ -285,19 +357,19 @@ await client.session.prompt({
### Files
| Method | Description | Response |
| ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ query })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files and directories by name | `string[]` (paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ query })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.status({ query? })` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
| Method | Description | Response |
| ------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ query })` | Sucht Text in Dateien | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Findet Dateien und Verzeichnisse nach Namen | `string[]` (paths) |
| `find.symbols({ query })` | Findet Workspace-Symbole | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ query })` | Liest eine Datei | `{ type: "raw" \| "patch", content: string }` |
| `file.status({ query? })` | Ruft Status fuer getrackte Dateien ab | <a href={typesUrl}><code>File[]</code></a> |
`find.files` supports a few optional query fields:
`find.files` unterstuetzt einige optionale Query-Felder:
- `type`: `"file"` or `"directory"`
- `directory`: override the project root for the search
- `limit`: max results (1200)
- `type`: `"file"` oder `"directory"`
- `directory`: Ueberschreibt das Projekt-Root fuer die Suche
- `limit`: Maximale Ergebnisse (1200)
---
@@ -326,17 +398,17 @@ const content = await client.file.read({
### TUI
| Method | Description | Response |
| ------------------------------ | ------------------------- | --------- |
| `tui.appendPrompt({ body })` | Append text to the prompt | `boolean` |
| `tui.openHelp()` | Open the help dialog | `boolean` |
| `tui.openSessions()` | Open the session selector | `boolean` |
| `tui.openThemes()` | Open the theme selector | `boolean` |
| `tui.openModels()` | Open the model selector | `boolean` |
| `tui.submitPrompt()` | Submit the current prompt | `boolean` |
| `tui.clearPrompt()` | Clear the prompt | `boolean` |
| `tui.executeCommand({ body })` | Execute a command | `boolean` |
| `tui.showToast({ body })` | Show toast notification | `boolean` |
| Method | Description | Response |
| ------------------------------ | ------------------------------ | --------- |
| `tui.appendPrompt({ body })` | Haengt Text an den Prompt an | `boolean` |
| `tui.openHelp()` | Oeffnet den Hilfedialog | `boolean` |
| `tui.openSessions()` | Oeffnet die Session-Auswahl | `boolean` |
| `tui.openThemes()` | Oeffnet die Theme-Auswahl | `boolean` |
| `tui.openModels()` | Oeffnet die Modell-Auswahl | `boolean` |
| `tui.submitPrompt()` | Sendet den aktuellen Prompt ab | `boolean` |
| `tui.clearPrompt()` | Leert den Prompt | `boolean` |
| `tui.executeCommand({ body })` | Fuehrt einen Befehl aus | `boolean` |
| `tui.showToast({ body })` | Zeigt Toast-Benachrichtigung | `boolean` |
---
@@ -357,9 +429,9 @@ await client.tui.showToast({
### Auth
| Method | Description | Response |
| ------------------- | ------------------------------ | --------- |
| `auth.set({ ... })` | Set authentication credentials | `boolean` |
| Method | Description | Response |
| ------------------- | ----------------------------- | --------- |
| `auth.set({ ... })` | Setzt Authentifizierungsdaten | `boolean` |
---
@@ -378,7 +450,7 @@ await client.auth.set({
| Method | Description | Response |
| ------------------- | ------------------------- | ------------------------- |
| `event.subscribe()` | Server-sent events stream | Server-sent events stream |
| `event.subscribe()` | Server-Sent Events Stream | Server-sent events stream |
---

View File

@@ -93,28 +93,28 @@ Der opencode-Server stellt folgende APIs bereit.
### Global
| Method | Path | Description | Response |
| ------ | ---------------- | ------------------------------ | ------------------------------------ |
| `GET` | `/global/health` | Get server health and version | `{ healthy: true, version: string }` |
| `GET` | `/global/event` | Get global events (SSE stream) | Event stream |
| Method | Path | Description | Response |
| ------ | ---------------- | ----------------------------------- | ------------------------------------ |
| `GET` | `/global/health` | Ruft Server-Status und Version ab | `{ healthy: true, version: string }` |
| `GET` | `/global/event` | Ruft globale Events ab (SSE-Stream) | Event stream |
---
### Project
| Method | Path | Description | Response |
| ------ | ------------------ | ----------------------- | --------------------------------------------- |
| `GET` | `/project` | List all projects | <a href={typesUrl}><code>Project[]</code></a> |
| `GET` | `/project/current` | Get the current project | <a href={typesUrl}><code>Project</code></a> |
| Method | Path | Description | Response |
| ------ | ------------------ | ---------------------------- | --------------------------------------------- |
| `GET` | `/project` | Listet alle Projekte | <a href={typesUrl}><code>Project[]</code></a> |
| `GET` | `/project/current` | Ruft das aktuelle Projekt ab | <a href={typesUrl}><code>Project</code></a> |
---
### Path & VCS
| Method | Path | Description | Response |
| ------ | ------- | ------------------------------------ | ------------------------------------------- |
| `GET` | `/path` | Get the current path | <a href={typesUrl}><code>Path</code></a> |
| `GET` | `/vcs` | Get VCS info for the current project | <a href={typesUrl}><code>VcsInfo</code></a> |
| Method | Path | Description | Response |
| ------ | ------- | ------------------------------------------- | ------------------------------------------- |
| `GET` | `/path` | Ruft den aktuellen Pfad ab | <a href={typesUrl}><code>Path</code></a> |
| `GET` | `/vcs` | Ruft VCS-Infos fuer das aktuelle Projekt ab | <a href={typesUrl}><code>VcsInfo</code></a> |
---
@@ -122,87 +122,87 @@ Der opencode-Server stellt folgende APIs bereit.
| Method | Path | Description | Response |
| ------ | ------------------- | ---------------------------- | --------- |
| `POST` | `/instance/dispose` | Dispose the current instance | `boolean` |
| `POST` | `/instance/dispose` | Beendet die aktuelle Instanz | `boolean` |
---
### Konfiguration
| Method | Path | Description | Response |
| ------- | ------------------- | --------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `PATCH` | `/config` | Update config | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
| Method | Path | Description | Response |
| ------- | ------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `GET` | `/config` | Ruft Konfigurationsinfos ab | <a href={typesUrl}><code>Config</code></a> |
| `PATCH` | `/config` | Aktualisiert Konfiguration | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | Listet Provider und Standard-Modelle | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
---
### Anbieter
| Method | Path | Description | Response |
| ------ | -------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------- |
| `GET` | `/provider` | List all providers | `{ all: `<a href={typesUrl}>Provider[]</a>`, default: {...}, connected: string[] }` |
| `GET` | `/provider/auth` | Get provider authentication methods | `{ [providerID: string]: `<a href={typesUrl}>ProviderAuthMethod[]</a>` }` |
| `POST` | `/provider/{id}/oauth/authorize` | Authorize a provider using OAuth | <a href={typesUrl}><code>ProviderAuthAuthorization</code></a> |
| `POST` | `/provider/{id}/oauth/callback` | Handle OAuth callback for a provider | `boolean` |
| Method | Path | Description | Response |
| ------ | -------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
| `GET` | `/provider` | Listet alle Provider | `{ all: `<a href={typesUrl}>Provider[]</a>`, default: {...}, connected: string[] }` |
| `GET` | `/provider/auth` | Ruft Authentifizierungsmethoden der Provider ab | `{ [providerID: string]: `<a href={typesUrl}>ProviderAuthMethod[]</a>` }` |
| `POST` | `/provider/{id}/oauth/authorize` | Autorisiert einen Provider per OAuth | <a href={typesUrl}><code>ProviderAuthAuthorization</code></a> |
| `POST` | `/provider/{id}/oauth/callback` | Behandelt OAuth-Callback fuer einen Provider | `boolean` |
---
### Sitzungen
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | List all sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` |
| `GET` | `/session/:id` | Get session details | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Get a session's child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns <a href={typesUrl}><code>Todo[]</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
| `POST` | `/session/:id/share` | Share a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response, remember? }`, returns `boolean` |
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | Listet alle Sitzungen | Gibt <a href={typesUrl}><code>Session[]</code></a> zurueck |
| `POST` | `/session` | Erstellt eine neue Sitzung | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Ruft Status aller Sitzungen ab | Gibt `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` zurueck |
| `GET` | `/session/:id` | Ruft Sitzungsdetails ab | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `DELETE` | `/session/:id` | Loescht eine Sitzung und alle Daten | Gibt `boolean` zurueck |
| `PATCH` | `/session/:id` | Aktualisiert Sitzungseigenschaften | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Ruft Kind-Sitzungen einer Sitzung ab | Gibt <a href={typesUrl}><code>Session[]</code></a> zurueck |
| `GET` | `/session/:id/todo` | Ruft die Todo-Liste einer Sitzung ab | Gibt <a href={typesUrl}><code>Todo[]</code></a> zurueck |
| `POST` | `/session/:id/init` | Analysiert App und erstellt `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Forkt eine bestehende Sitzung an einer Nachricht | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Bricht eine laufende Sitzung ab | Gibt `boolean` zurueck |
| `POST` | `/session/:id/share` | Teilt eine Sitzung | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `DELETE` | `/session/:id/share` | Hebt Teilen einer Sitzung auf | Gibt <a href={typesUrl}><code>Session</code></a> zurueck |
| `GET` | `/session/:id/diff` | Ruft den Diff fuer diese Sitzung ab | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Fasst die Sitzung zusammen | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Setzt eine Nachricht zurueck | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Stellt alle zurueckgesetzten Nachrichten wieder her | Gibt `boolean` zurueck |
| `POST` | `/session/:id/permissions/:permissionID` | Antwortet auf eine Berechtigungsanfrage | body: `{ response, remember? }`, returns `boolean` |
---
### Nachrichten
| Method | Path | Description | Notes |
| ------ | --------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session/:id/message` | List messages in a session | query: `limit?`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `POST` | `/session/:id/message` | Send a message and wait for response | body: `{ messageID?, model?, agent?, noReply?, system?, tools?, parts }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/prompt_async` | Send a message asynchronously (no wait) | body: same as `/session/:id/message`, returns `204 No Content` |
| `POST` | `/session/:id/command` | Execute a slash command | body: `{ messageID?, agent?, model?, command, arguments }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/shell` | Run a shell command | body: `{ agent, model?, command }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| Method | Path | Description | Notes |
| ------ | --------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session/:id/message` | Listet Nachrichten in einer Sitzung | query: `limit?`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `POST` | `/session/:id/message` | Sendet eine Nachricht und wartet auf Antwort | body: `{ messageID?, model?, agent?, noReply?, system?, tools?, parts }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `GET` | `/session/:id/message/:messageID` | Ruft Nachrichtendetails ab | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/prompt_async` | Sendet eine Nachricht asynchron (ohne Warten) | body: same as `/session/:id/message`, returns `204 No Content` |
| `POST` | `/session/:id/command` | Fuehrt einen Slash-Befehl aus | body: `{ messageID?, agent?, model?, command, arguments }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/shell` | Fuehrt einen Shell-Befehl aus | body: `{ agent, model?, command }`, returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
---
### Befehle
| Method | Path | Description | Response |
| ------ | ---------- | ----------------- | --------------------------------------------- |
| `GET` | `/command` | List all commands | <a href={typesUrl}><code>Command[]</code></a> |
| Method | Path | Description | Response |
| ------ | ---------- | ------------------- | --------------------------------------------- |
| `GET` | `/command` | Listet alle Befehle | <a href={typesUrl}><code>Command[]</code></a> |
---
### Dateien
| Method | Path | Description | Response |
| ------ | ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files and directories by name | `string[]` (paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | List files and directories | <a href={typesUrl}><code>FileNode[]</code></a> |
| `GET` | `/file/content?path=<p>` | Read a file | <a href={typesUrl}><code>FileContent</code></a> |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
| Method | Path | Description | Response |
| ------ | ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Sucht Text in Dateien | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Findet Dateien und Verzeichnisse nach Namen | `string[]` (paths) |
| `GET` | `/find/symbol?query=<q>` | Findet Workspace-Symbole | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | Listet Dateien und Verzeichnisse | <a href={typesUrl}><code>FileNode[]</code></a> |
| `GET` | `/file/content?path=<p>` | Liest eine Datei | <a href={typesUrl}><code>FileContent</code></a> |
| `GET` | `/file/status` | Ruft Status fuer getrackte Dateien ab | <a href={typesUrl}><code>File[]</code></a> |
#### `/find/file` Abfrageparameter
@@ -216,71 +216,71 @@ Der opencode-Server stellt folgende APIs bereit.
### Werkzeuge (Experimentell)
| Method | Path | Description | Response |
| ------ | ------------------------------------------- | ---------------------------------------- | -------------------------------------------- |
| `GET` | `/experimental/tool/ids` | List all tool IDs | <a href={typesUrl}><code>ToolIDs</code></a> |
| `GET` | `/experimental/tool?provider=<p>&model=<m>` | List tools with JSON schemas for a model | <a href={typesUrl}><code>ToolList</code></a> |
| Method | Path | Description | Response |
| ------ | ------------------------------------------- | --------------------------------------------- | -------------------------------------------- |
| `GET` | `/experimental/tool/ids` | Listet alle Tool-IDs | <a href={typesUrl}><code>ToolIDs</code></a> |
| `GET` | `/experimental/tool?provider=<p>&model=<m>` | Listet Tools mit JSON-Schemas fuer ein Modell | <a href={typesUrl}><code>ToolList</code></a> |
---
### LSP, Formatierer & MCP
| Method | Path | Description | Response |
| ------ | ------------ | -------------------------- | -------------------------------------------------------- |
| `GET` | `/lsp` | Get LSP server status | <a href={typesUrl}><code>LSPStatus[]</code></a> |
| `GET` | `/formatter` | Get formatter status | <a href={typesUrl}><code>FormatterStatus[]</code></a> |
| `GET` | `/mcp` | Get MCP server status | `{ [name: string]: `<a href={typesUrl}>MCPStatus</a>` }` |
| `POST` | `/mcp` | Add MCP server dynamically | body: `{ name, config }`, returns MCP status object |
| Method | Path | Description | Response |
| ------ | ------------ | -------------------------------- | -------------------------------------------------------- |
| `GET` | `/lsp` | Ruft LSP-Server-Status ab | <a href={typesUrl}><code>LSPStatus[]</code></a> |
| `GET` | `/formatter` | Ruft Formatter-Status ab | <a href={typesUrl}><code>FormatterStatus[]</code></a> |
| `GET` | `/mcp` | Ruft MCP-Server-Status ab | `{ [name: string]: `<a href={typesUrl}>MCPStatus</a>` }` |
| `POST` | `/mcp` | Fuegt MCP-Server dynamisch hinzu | body: `{ name, config }`, returns MCP status object |
---
### Agenten
| Method | Path | Description | Response |
| ------ | -------- | ------------------------- | ------------------------------------------- |
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
| Method | Path | Description | Response |
| ------ | -------- | -------------------------------- | ------------------------------------------- |
| `GET` | `/agent` | Listet alle verfuegbaren Agenten | <a href={typesUrl}><code>Agent[]</code></a> |
---
### Logging
| Method | Path | Description | Response |
| ------ | ------ | ------------------------------------------------------------ | --------- |
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
| Method | Path | Description | Response |
| ------ | ------ | ----------------------------------------------------------------- | --------- |
| `POST` | `/log` | Schreibt Log-Eintrag. Body: `{ service, level, message, extra? }` | `boolean` |
---
### TUI
| Method | Path | Description | Response |
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
| Method | Path | Description | Response |
| ------ | ----------------------- | ----------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Haengt Text an den Prompt an | `boolean` |
| `POST` | `/tui/open-help` | Oeffnet den Hilfedialog | `boolean` |
| `POST` | `/tui/open-sessions` | Oeffnet die Session-Auswahl | `boolean` |
| `POST` | `/tui/open-themes` | Oeffnet die Theme-Auswahl | `boolean` |
| `POST` | `/tui/open-models` | Oeffnet die Modell-Auswahl | `boolean` |
| `POST` | `/tui/submit-prompt` | Sendet den aktuellen Prompt ab | `boolean` |
| `POST` | `/tui/clear-prompt` | Leert den Prompt | `boolean` |
| `POST` | `/tui/execute-command` | Fuehrt einen Befehl aus (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Zeigt Toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wartet auf die naechste Kontrollanfrage | Control request object |
| `POST` | `/tui/control/response` | Antwortet auf eine Kontrollanfrage (`{ body }`) | `boolean` |
---
### Authentifizierung
| Method | Path | Description | Response |
| ------ | ----------- | --------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
| Method | Path | Description | Response |
| ------ | ----------- | ------------------------------------------------------------------------ | --------- |
| `PUT` | `/auth/:id` | Setzt Authentifizierungsdaten. Body muss dem Provider-Schema entsprechen | `boolean` |
---
### Events
| Method | Path | Description | Response |
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
| Method | Path | Description | Response |
| ------ | -------- | ------------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-Sent Events Stream. Erstes Event ist `server.connected`, dann Bus-Events | Server-sent events stream |
---
@@ -288,4 +288,4 @@ Der opencode-Server stellt folgende APIs bereit.
| Method | Path | Description | Response |
| ------ | ------ | ------------------------- | --------------------------- |
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
| `GET` | `/doc` | OpenAPI 3.1 Spezifikation | HTML page with OpenAPI spec |

Some files were not shown because too many files have changed in this diff Show More