Compare commits

..

1 Commits

Author SHA1 Message Date
Dax Raad
50004d1f94 refactor: replace Bun.sleep with node timers 2026-02-24 23:54:49 -05:00
103 changed files with 555 additions and 1558 deletions

View File

@@ -65,9 +65,9 @@ jobs:
"packages/web/src/content/docs/*/*.mdx": "allow",
".opencode": "allow",
".opencode/agent": "allow",
".opencode/glossary": "allow",
".opencode/agent/glossary": "allow",
".opencode/agent/translator.md": "allow",
".opencode/glossary/*.md": "allow"
".opencode/agent/glossary/*.md": "allow"
},
"edit": {
"*": "deny",
@@ -76,7 +76,7 @@ jobs:
"glob": {
"*": "deny",
"packages/web/src/content/docs*": "allow",
".opencode/glossary*": "allow"
".opencode/agent/glossary*": "allow"
},
"task": {
"*": "deny",
@@ -90,7 +90,7 @@ jobs:
"read": {
"*": "deny",
".opencode/agent/translator.md": "allow",
".opencode/glossary/*.md": "allow"
".opencode/agent/glossary/*.md": "allow"
}
}
}

View File

@@ -42,17 +42,15 @@ jobs:
throw error;
}
// Parse the .td file for vouched and denounced users
const vouched = new Set();
// Parse the .td file for denounced users
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (!trimmed.startsWith('-')) continue;
const isDenounced = trimmed.startsWith('-');
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
const rest = trimmed.slice(1).trim();
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
@@ -67,50 +65,32 @@ jobs:
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
if (isDenounced) {
denounced.set(username.toLowerCase(), reason);
continue;
}
vouched.add(username.toLowerCase());
denounced.set(username.toLowerCase(), reason);
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason !== undefined) {
// Author is denounced — close the issue
const body = 'This issue has been automatically closed.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned',
});
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
if (reason === undefined) {
core.info(`User ${author} is not denounced. Allowing issue.`);
return;
}
// Author is positively vouched — add label
if (!vouched.has(author.toLowerCase())) {
core.info(`User ${author} is not denounced or vouched. Allowing issue.`);
return;
}
// Author is denounced — close the issue
const body = 'This issue has been automatically closed.';
await github.rest.issues.addLabels({
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['Vouched'],
body,
});
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned',
});
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);

View File

@@ -6,7 +6,6 @@ on:
permissions:
contents: read
issues: write
pull-requests: write
jobs:
@@ -43,17 +42,15 @@ jobs:
throw error;
}
// Parse the .td file for vouched and denounced users
const vouched = new Set();
// Parse the .td file for denounced users
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (!trimmed.startsWith('-')) continue;
const isDenounced = trimmed.startsWith('-');
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
const rest = trimmed.slice(1).trim();
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
@@ -68,47 +65,29 @@ jobs:
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
if (isDenounced) {
denounced.set(username.toLowerCase(), reason);
continue;
}
vouched.add(username.toLowerCase());
denounced.set(username.toLowerCase(), reason);
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason !== undefined) {
// Author is denounced — close the PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: 'This pull request has been automatically closed.',
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
if (reason === undefined) {
core.info(`User ${author} is not denounced. Allowing PR.`);
return;
}
// Author is positively vouched — add label
if (!vouched.has(author.toLowerCase())) {
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
return;
}
await github.rest.issues.addLabels({
// Author is denounced — close the PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['Vouched'],
body: 'This pull request has been automatically closed.',
});
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.info(`Closed PR #${prNumber} from denounced user ${author}`);

View File

@@ -33,6 +33,5 @@ jobs:
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
roles: admin,maintain
env:
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}

View File

@@ -13,7 +13,7 @@ Requirements:
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
- Also preserve every term listed in the Do-Not-Translate glossary below.
- Also apply locale-specific guidance from `.opencode/glossary/<locale>.md` when available (for example, `zh-cn.md`).
- Also apply locale-specific guidance from `.opencode/agent/glossary/<locale>.md` when available (for example, `zh-cn.md`).
- Do not modify fenced code blocks.
- Output ONLY the translation (no commentary).

View File

@@ -25,7 +25,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -75,7 +75,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -109,7 +109,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -136,7 +136,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -160,7 +160,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -184,7 +184,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -217,7 +217,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -246,7 +246,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -262,7 +262,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.2.14",
"version": "1.2.11",
"bin": {
"opencode": "./bin/opencode",
},
@@ -376,7 +376,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -396,7 +396,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.2.14",
"version": "1.2.11",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -407,7 +407,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -420,7 +420,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -462,7 +462,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"zod": "catalog:",
},
@@ -473,7 +473,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

View File

@@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context"
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { spawn } from "node:child_process"
import { setTimeout as sleep } from "node:timers/promises"
type GitHubAuthor = {
login: string
@@ -281,7 +282,7 @@ async function assertOpencodeConnected() {
connected = true
break
} catch (e) {}
await Bun.sleep(300)
await sleep(300)
} while (retry++ < 30)
if (!connected) {

View File

@@ -101,7 +101,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
})
const zenLiteProduct = new stripe.Product("ZenLite", {
name: "OpenCode Go",
name: "OpenCode Lite",
})
const zenLitePrice = new stripe.Price("ZenLitePrice", {
product: zenLiteProduct.id,

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.2.14",
"version": "1.2.11",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,28 +1,28 @@
import { AppIcon } from "@opencode-ai/ui/app-icon"
import { Button } from "@opencode-ai/ui/button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Popover } from "@opencode-ai/ui/popover"
import { Spinner } from "@opencode-ai/ui/spinner"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/util/path"
import { useParams } from "@solidjs/router"
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
import { useCommand } from "@/context/command"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLanguage } from "@/context/language"
import { useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path"
import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
import { AppIcon } from "@opencode-ai/ui/app-icon"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Popover } from "@opencode-ai/ui/popover"
import { TextField } from "@opencode-ai/ui/text-field"
import { Keybind } from "@opencode-ai/ui/keybind"
import { showToast } from "@opencode-ai/ui/toast"
import { StatusPopover } from "../status-popover"
const OPEN_APPS = [
@@ -45,67 +45,32 @@ type OpenApp = (typeof OPEN_APPS)[number]
type OS = "macos" | "windows" | "linux" | "unknown"
const MAC_APPS = [
{
id: "vscode",
label: "VS Code",
icon: "vscode",
openWith: "Visual Studio Code",
},
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
{
id: "antigravity",
label: "Antigravity",
icon: "antigravity",
openWith: "Antigravity",
},
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
{
id: "android-studio",
label: "Android Studio",
icon: "android-studio",
openWith: "Android Studio",
},
{
id: "sublime-text",
label: "Sublime Text",
icon: "sublime-text",
openWith: "Sublime Text",
},
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
] as const
const WINDOWS_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
{
id: "powershell",
label: "PowerShell",
icon: "powershell",
openWith: "powershell",
},
{
id: "sublime-text",
label: "Sublime Text",
icon: "sublime-text",
openWith: "Sublime Text",
},
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
] as const
const LINUX_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
{
id: "sublime-text",
label: "Sublime Text",
icon: "sublime-text",
openWith: "Sublime Text",
},
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
] as const
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
@@ -248,9 +213,7 @@ export function SessionHeader() {
const view = createMemo(() => layout.view(sessionKey))
const os = createMemo(() => detectOS(platform))
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
finder: true,
})
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
const apps = createMemo(() => {
if (os() === "macos") return MAC_APPS
@@ -296,34 +259,18 @@ export function SessionHeader() {
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
const [menu, setMenu] = createStore({ open: false })
const [openRequest, setOpenRequest] = createStore({
app: undefined as OpenApp | undefined,
})
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
const opening = createMemo(() => openRequest.app !== undefined)
createEffect(() => {
const value = prefs.app
if (options().some((o) => o.id === value)) return
setPrefs("app", options()[0]?.id ?? "finder")
})
const openDir = (app: OpenApp) => {
if (opening() || !canOpen() || !platform.openPath) return
const directory = projectDirectory()
if (!directory) return
if (!canOpen()) return
const item = options().find((o) => o.id === app)
const openWith = item && "openWith" in item ? item.openWith : undefined
setOpenRequest("app", app)
platform
.openPath(directory, openWith)
.catch((err: unknown) => showRequestError(language, err))
.finally(() => {
setOpenRequest("app", undefined)
})
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err))
}
const copyPath = () => {
@@ -368,9 +315,7 @@ export function SessionHeader() {
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
{language.t("session.header.search.placeholder", {
project: name(),
})}
{language.t("session.header.search.placeholder", { project: name() })}
</span>
</div>
@@ -412,21 +357,12 @@ export function SessionHeader() {
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default"
classList={{
"bg-surface-raised-base-active": opening(),
}}
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
onClick={() => openDir(current().id)}
disabled={opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<div class="flex size-5 shrink-0 items-center justify-center">
<Show
when={opening()}
fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />}
>
<Spinner class="size-3.5 text-icon-base" />
</Show>
<AppIcon id={current().icon} class="size-4" />
</div>
<span class="text-12-regular text-text-strong">Open</span>
</Button>
@@ -441,11 +377,7 @@ export function SessionHeader() {
as={IconButton}
icon="chevron-down"
variant="ghost"
disabled={opening()}
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
classList={{
"bg-surface-raised-base-active": opening(),
}}
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover"
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
@@ -463,7 +395,6 @@ export function SessionHeader() {
{(o) => (
<DropdownMenu.RadioItem
value={o.id}
disabled={opening()}
onSelect={() => {
setMenu("open", false)
openDir(o.id)

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Preporučeno",
"dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge",
"dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max",
"dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot",
"dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju",
"dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke",
"dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore",
"dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera",

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Anbefalet",
"dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere",
"dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max",
"dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot",
"dialog.provider.copilot.note": "Claude-modeller til kodningsassistance",
"dialog.provider.openai.note": "GPT-modeller til hurtige, kompetente generelle AI-opgaver",
"dialog.provider.google.note": "Gemini-modeller til hurtige, strukturerede svar",
"dialog.provider.openrouter.note": "Få adgang til alle understøttede modeller fra én udbyder",

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Recommended",
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
"dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot",
"dialog.provider.copilot.note": "Claude models for coding assistance",
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Recomendado",
"dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más",
"dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max",
"dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot",
"dialog.provider.copilot.note": "Modelos Claude para asistencia de codificación",
"dialog.provider.openai.note": "Modelos GPT para tareas de IA generales rápidas y capaces",
"dialog.provider.google.note": "Modelos Gemini para respuestas rápidas y estructuradas",
"dialog.provider.openrouter.note": "Accede a todos los modelos soportados desde un solo proveedor",

View File

@@ -103,7 +103,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Anbefalt",
"dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer",
"dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max",
"dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot",
"dialog.provider.copilot.note": "Claude-modeller for kodeassistanse",
"dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver",
"dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar",
"dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør",

View File

@@ -92,7 +92,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Zalecane",
"dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne",
"dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max",
"dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot",
"dialog.provider.copilot.note": "Modele Claude do pomocy w kodowaniu",
"dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI",
"dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi",
"dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy",

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "Рекомендуемые",
"dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие",
"dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max",
"dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot",
"dialog.provider.copilot.note": "Модели Claude для помощи в кодировании",
"dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ",
"dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов",
"dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера",

View File

@@ -100,7 +100,7 @@ export const dict = {
"dialog.provider.tag.recommended": "แนะนำ",
"dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ",
"dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max",
"dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot",
"dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด",
"dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ",
"dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง",
"dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว",

View File

@@ -254,13 +254,12 @@ export default function Page() {
const msgs = visibleUserMessages()
if (msgs.length === 0) return
const current = store.messageId
const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length
const currentIndex = base === -1 ? msgs.length : base
const targetIndex = currentIndex + offset
if (targetIndex < 0 || targetIndex > msgs.length) return
const current = activeMessage()
const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
if (targetIndex < 0 || targetIndex >= msgs.length) return
if (targetIndex === msgs.length) {
if (targetIndex === msgs.length - 1) {
resumeScroll()
return
}

View File

@@ -376,7 +376,6 @@ export function MessageTimeline(props: {
>
<Show when={showHeader()}>
<div
data-session-title
classList={{
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
"w-full": true,

View File

@@ -45,9 +45,7 @@ export const useSessionHashScroll = (input: {
const a = el.getBoundingClientRect()
const b = root.getBoundingClientRect()
const sticky = root.querySelector("[data-session-title]")
const inset = sticky instanceof HTMLElement ? sticky.offsetHeight : 0
const top = Math.max(0, a.top - b.top + root.scrollTop - inset)
const top = a.top - b.top + root.scrollTop
root.scrollTo({ top, behavior })
return true
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -344,8 +344,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "كتابة الكاش",
"workspace.usage.breakdown.output": "الخرج",
"workspace.usage.breakdown.reasoning": "المنطق",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "الاشتراك (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "التكلفة",
@@ -491,26 +491,21 @@ export const dict = {
"workspace.lite.time.minute": "دقيقة",
"workspace.lite.time.minutes": "دقائق",
"workspace.lite.time.fewSeconds": "بضع ثوان",
"workspace.lite.subscription.title": "اشتراك Go",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
"workspace.lite.subscription.title": "اشتراك Lite",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Lite.",
"workspace.lite.subscription.manage": "إدارة الاشتراك",
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
"workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي",
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
"workspace.lite.subscription.resetsIn": "إعادة تعيين في",
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
"workspace.lite.other.title": "اشتراك Go",
"workspace.lite.other.title": "اشتراك Lite",
"workspace.lite.other.message":
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.title": "OpenCode Go",
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Lite. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.",
"workspace.lite.promo.modelsTitle": "ما يتضمنه",
"workspace.lite.promo.footer":
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.",
"workspace.lite.promo.subscribe": "الاشتراك في Go",
"احصل على وصول إلى أفضل النماذج المفتوحة — Kimi K2.5، و GLM-5، و MiniMax M2.5 — مع حدود استخدام سخية مقابل $10 شهريًا.",
"workspace.lite.promo.subscribe": "الاشتراك في Lite",
"workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...",
"download.title": "OpenCode | تنزيل",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Escrita em Cache",
"workspace.usage.breakdown.output": "Saída",
"workspace.usage.breakdown.reasoning": "Raciocínio",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "assinatura (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Custo",
@@ -497,26 +497,21 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "alguns segundos",
"workspace.lite.subscription.title": "Assinatura Go",
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
"workspace.lite.subscription.title": "Assinatura Lite",
"workspace.lite.subscription.message": "Você assina o OpenCode Lite.",
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensal",
"workspace.lite.subscription.resetsIn": "Reinicia em",
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
"workspace.lite.other.title": "Assinatura Go",
"workspace.lite.other.title": "Assinatura Lite",
"workspace.lite.other.message":
"Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.title": "OpenCode Go",
"Outro membro neste workspace já assina o OpenCode Lite. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"O OpenCode Go é uma assinatura de $10 por mês que fornece acesso confiável a modelos abertos de codificação populares com limites de uso generosos.",
"workspace.lite.promo.modelsTitle": "O que está incluído",
"workspace.lite.promo.footer":
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.",
"workspace.lite.promo.subscribe": "Assinar Go",
"Tenha acesso aos melhores modelos abertos — Kimi K2.5, GLM-5 e MiniMax M2.5 — com limites de uso generosos por $10 por mês.",
"workspace.lite.promo.subscribe": "Assinar Lite",
"workspace.lite.promo.subscribing": "Redirecionando...",
"download.title": "OpenCode | Baixar",

View File

@@ -347,8 +347,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Ræsonnement",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Omkostninger",
@@ -495,26 +495,21 @@ export const dict = {
"workspace.lite.time.minute": "minut",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "et par sekunder",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.title": "Lite-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
"workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug",
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
"workspace.lite.subscription.resetsIn": "Nulstiller i",
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.other.title": "Lite-abonnement",
"workspace.lite.other.message":
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Lite. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go er et abonnement til $10 om måneden, der giver pålidelig adgang til populære åbne kodningsmodeller med generøse forbrugsgrænser.",
"workspace.lite.promo.modelsTitle": "Hvad er inkluderet",
"workspace.lite.promo.footer":
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.",
"workspace.lite.promo.subscribe": "Abonner på Go",
"Få adgang til de bedste åbne modeller — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse forbrugsgrænser for $10 om måneden.",
"workspace.lite.promo.subscribe": "Abonner på Lite",
"workspace.lite.promo.subscribing": "Omdirigerer...",
"download.title": "OpenCode | Download",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Write",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "Abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Kosten",
@@ -497,26 +497,21 @@ export const dict = {
"workspace.lite.time.minute": "Minute",
"workspace.lite.time.minutes": "Minuten",
"workspace.lite.time.fewSeconds": "einige Sekunden",
"workspace.lite.subscription.title": "Go-Abonnement",
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
"workspace.lite.subscription.title": "Lite-Abonnement",
"workspace.lite.subscription.message": "Du hast OpenCode Lite abonniert.",
"workspace.lite.subscription.manage": "Abo verwalten",
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
"workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung",
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
"workspace.lite.subscription.resetsIn": "Setzt zurück in",
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
"workspace.lite.other.title": "Go-Abonnement",
"workspace.lite.other.title": "Lite-Abonnement",
"workspace.lite.other.message":
"Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.title": "OpenCode Go",
"Ein anderes Mitglied in diesem Workspace hat OpenCode Lite bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go ist ein Abonnement für $10 pro Monat, das zuverlässigen Zugriff auf beliebte offene Coding-Modelle mit großzügigen Nutzungslimits bietet.",
"workspace.lite.promo.modelsTitle": "Was enthalten ist",
"workspace.lite.promo.footer":
"Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.",
"workspace.lite.promo.subscribe": "Go abonnieren",
"Erhalte Zugriff auf die besten offenen Modelle — Kimi K2.5, GLM-5 und MiniMax M2.5 — mit großzügigen Nutzungslimits für $10 pro Monat.",
"workspace.lite.promo.subscribe": "Lite abonnieren",
"workspace.lite.promo.subscribing": "Leite weiter...",
"download.title": "OpenCode | Download",

View File

@@ -341,8 +341,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Write",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "subscription (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Cost",
@@ -489,26 +489,21 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "a few seconds",
"workspace.lite.subscription.title": "Go Subscription",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
"workspace.lite.subscription.title": "Lite Subscription",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Lite.",
"workspace.lite.subscription.manage": "Manage Subscription",
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
"workspace.lite.subscription.weeklyUsage": "Weekly Usage",
"workspace.lite.subscription.monthlyUsage": "Monthly Usage",
"workspace.lite.subscription.resetsIn": "Resets in",
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.other.title": "Go Subscription",
"workspace.lite.other.title": "Lite Subscription",
"workspace.lite.other.message":
"Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.",
"workspace.lite.promo.title": "OpenCode Go",
"Another member in this workspace is already subscribed to OpenCode Lite. Only one member per workspace can subscribe.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits.",
"workspace.lite.promo.modelsTitle": "What's Included",
"workspace.lite.promo.footer":
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.",
"workspace.lite.promo.subscribe": "Subscribe to Go",
"Get access to the best open models — Kimi K2.5, GLM-5, and MiniMax M2.5 — with generous usage limits for $10 per month.",
"workspace.lite.promo.subscribe": "Subscribe to Lite",
"workspace.lite.promo.subscribing": "Redirecting...",
"download.title": "OpenCode | Download",

View File

@@ -350,8 +350,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Escritura de Caché",
"workspace.usage.breakdown.output": "Salida",
"workspace.usage.breakdown.reasoning": "Razonamiento",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "suscripción (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Costo",
@@ -498,26 +498,21 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "unos pocos segundos",
"workspace.lite.subscription.title": "Suscripción Go",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
"workspace.lite.subscription.title": "Suscripción Lite",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Lite.",
"workspace.lite.subscription.manage": "Gestionar Suscripción",
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensual",
"workspace.lite.subscription.resetsIn": "Se reinicia en",
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
"workspace.lite.other.title": "Suscripción Go",
"workspace.lite.other.title": "Suscripción Lite",
"workspace.lite.other.message":
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.title": "OpenCode Go",
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Lite. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go es una suscripción de $10 al mes que proporciona acceso confiable a modelos de codificación abiertos populares con generosos límites de uso.",
"workspace.lite.promo.modelsTitle": "Qué incluye",
"workspace.lite.promo.footer":
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.",
"workspace.lite.promo.subscribe": "Suscribirse a Go",
"Obtén acceso a los mejores modelos abiertos — Kimi K2.5, GLM-5 y MiniMax M2.5 — con generosos límites de uso por $10 al mes.",
"workspace.lite.promo.subscribe": "Suscribirse a Lite",
"workspace.lite.promo.subscribing": "Redirigiendo...",
"download.title": "OpenCode | Descargar",

View File

@@ -355,8 +355,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Écriture cache",
"workspace.usage.breakdown.output": "Sortie",
"workspace.usage.breakdown.reasoning": "Raisonnement",
"workspace.usage.subscription": "Black ({{amount}} $)",
"workspace.usage.lite": "Go ({{amount}} $)",
"workspace.usage.subscription": "abonnement ({{amount}} $)",
"workspace.usage.lite": "lite ({{amount}} $)",
"workspace.usage.byok": "BYOK ({{amount}} $)",
"workspace.cost.title": "Coût",
@@ -506,8 +506,8 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "quelques secondes",
"workspace.lite.subscription.title": "Abonnement Go",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
"workspace.lite.subscription.title": "Abonnement Lite",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Lite.",
"workspace.lite.subscription.manage": "Gérer l'abonnement",
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
"workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire",
@@ -515,18 +515,13 @@ export const dict = {
"workspace.lite.subscription.resetsIn": "Réinitialisation dans",
"workspace.lite.subscription.useBalance":
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider":
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
"workspace.lite.other.title": "Abonnement Go",
"workspace.lite.other.title": "Abonnement Lite",
"workspace.lite.other.message":
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.title": "OpenCode Go",
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Lite. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go est un abonnement à 10 $ par mois qui offre un accès fiable aux modèles de codage ouverts populaires avec des limites d'utilisation généreuses.",
"workspace.lite.promo.modelsTitle": "Ce qui est inclus",
"workspace.lite.promo.footer":
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.",
"workspace.lite.promo.subscribe": "S'abonner à Go",
"Accédez aux meilleurs modèles ouverts — Kimi K2.5, GLM-5 et MiniMax M2.5 — avec des limites d'utilisation généreuses pour 10 $ par mois.",
"workspace.lite.promo.subscribe": "S'abonner à Lite",
"workspace.lite.promo.subscribing": "Redirection...",
"download.title": "OpenCode | Téléchargement",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Scrittura Cache",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "abbonamento (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Costo",
@@ -497,26 +497,21 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minuti",
"workspace.lite.time.fewSeconds": "pochi secondi",
"workspace.lite.subscription.title": "Abbonamento Go",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
"workspace.lite.subscription.title": "Abbonamento Lite",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Lite.",
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
"workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale",
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
"workspace.lite.subscription.resetsIn": "Si resetta tra",
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
"workspace.lite.other.title": "Abbonamento Go",
"workspace.lite.other.title": "Abbonamento Lite",
"workspace.lite.other.message":
"Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.title": "OpenCode Go",
"Un altro membro in questo workspace è già abbonato a OpenCode Lite. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go è un abbonamento a $10 al mese che fornisce un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"workspace.lite.promo.modelsTitle": "Cosa è incluso",
"workspace.lite.promo.footer":
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.",
"workspace.lite.promo.subscribe": "Abbonati a Go",
"Ottieni l'accesso ai migliori modelli aperti — Kimi K2.5, GLM-5 e MiniMax M2.5 — con limiti di utilizzo generosi per $10 al mese.",
"workspace.lite.promo.subscribe": "Abbonati a Lite",
"workspace.lite.promo.subscribing": "Reindirizzamento...",
"download.title": "OpenCode | Download",

View File

@@ -346,8 +346,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "キャッシュ書き込み",
"workspace.usage.breakdown.output": "出力",
"workspace.usage.breakdown.reasoning": "推論",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "サブスクリプション (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "コスト",
@@ -495,26 +495,21 @@ export const dict = {
"workspace.lite.time.minute": "分",
"workspace.lite.time.minutes": "分",
"workspace.lite.time.fewSeconds": "数秒",
"workspace.lite.subscription.title": "Goサブスクリプション",
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
"workspace.lite.subscription.title": "Liteサブスクリプション",
"workspace.lite.subscription.message": "あなたは OpenCode Lite を購読しています。",
"workspace.lite.subscription.manage": "サブスクリプションの管理",
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
"workspace.lite.subscription.weeklyUsage": "週間利用量",
"workspace.lite.subscription.monthlyUsage": "月間利用量",
"workspace.lite.subscription.resetsIn": "リセットまで",
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
"workspace.lite.other.title": "Goサブスクリプション",
"workspace.lite.other.title": "Liteサブスクリプション",
"workspace.lite.other.message":
"このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.title": "OpenCode Go",
"このワークスペースの別のメンバーが既に OpenCode Lite を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠で提供します。",
"workspace.lite.promo.modelsTitle": "含まれるもの",
"workspace.lite.promo.footer":
"このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。",
"workspace.lite.promo.subscribe": "Goを購読する",
"月額$10で、十分な利用枠が設けられた最高のオープンモデル — Kimi K2.5、GLM-5、および MiniMax M2.5 — にアクセスできます。",
"workspace.lite.promo.subscribe": "Liteを購読する",
"workspace.lite.promo.subscribing": "リダイレクト中...",
"download.title": "OpenCode | ダウンロード",

View File

@@ -343,8 +343,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "캐시 쓰기",
"workspace.usage.breakdown.output": "출력",
"workspace.usage.breakdown.reasoning": "추론",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "구독 (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "비용",
@@ -490,26 +490,21 @@ export const dict = {
"workspace.lite.time.minute": "분",
"workspace.lite.time.minutes": "분",
"workspace.lite.time.fewSeconds": "몇 초",
"workspace.lite.subscription.title": "Go 구독",
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
"workspace.lite.subscription.title": "Lite 구독",
"workspace.lite.subscription.message": "현재 OpenCode Lite를 구독 중입니다.",
"workspace.lite.subscription.manage": "구독 관리",
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
"workspace.lite.subscription.weeklyUsage": "주간 사용량",
"workspace.lite.subscription.monthlyUsage": "월간 사용량",
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
"workspace.lite.other.title": "Go 구독",
"workspace.lite.other.title": "Lite 구독",
"workspace.lite.other.message":
"이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.title": "OpenCode Go",
"이 워크스페이스의 다른 멤버가 이미 OpenCode Lite를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.",
"workspace.lite.promo.modelsTitle": "포함 내역",
"workspace.lite.promo.footer":
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.",
"workspace.lite.promo.subscribe": "Go 구독하기",
"월 $10의 넉넉한 사용 한도로 최고의 오픈 모델인 Kimi K2.5, GLM-5, MiniMax M2.5에 액세스하세요.",
"workspace.lite.promo.subscribe": "Lite 구독하기",
"workspace.lite.promo.subscribing": "리디렉션 중...",
"download.title": "OpenCode | 다운로드",

View File

@@ -347,8 +347,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Skrevet",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Resonnering",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "abonnement (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Kostnad",
@@ -495,26 +495,21 @@ export const dict = {
"workspace.lite.time.minute": "minutt",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "noen få sekunder",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.title": "Lite-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
"workspace.lite.subscription.weeklyUsage": "Ukentlig bruk",
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
"workspace.lite.subscription.resetsIn": "Nullstilles om",
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.other.title": "Lite-abonnement",
"workspace.lite.other.message":
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Lite. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go er et abonnement til $10 per måned som gir pålitelig tilgang til populære åpne kodemodeller med rause bruksgrenser.",
"workspace.lite.promo.modelsTitle": "Hva som er inkludert",
"workspace.lite.promo.footer":
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.",
"workspace.lite.promo.subscribe": "Abonner på Go",
"Få tilgang til de beste åpne modellene — Kimi K2.5, GLM-5 og MiniMax M2.5 — med generøse bruksgrenser for $10 per måned.",
"workspace.lite.promo.subscribe": "Abonner på Lite",
"workspace.lite.promo.subscribing": "Omdirigerer...",
"download.title": "OpenCode | Last ned",

View File

@@ -348,8 +348,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Zapis Cache",
"workspace.usage.breakdown.output": "Wyjście",
"workspace.usage.breakdown.reasoning": "Rozumowanie",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "subskrypcja (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Koszt",
@@ -496,26 +496,21 @@ export const dict = {
"workspace.lite.time.minute": "minuta",
"workspace.lite.time.minutes": "minut(y)",
"workspace.lite.time.fewSeconds": "kilka sekund",
"workspace.lite.subscription.title": "Subskrypcja Go",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
"workspace.lite.subscription.title": "Subskrypcja Lite",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Lite.",
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
"workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe",
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
"workspace.lite.subscription.resetsIn": "Resetuje się za",
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
"workspace.lite.other.title": "Subskrypcja Go",
"workspace.lite.other.title": "Subskrypcja Lite",
"workspace.lite.other.message":
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.title": "OpenCode Go",
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Lite. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go to subskrypcja za $10 miesięcznie, która zapewnia niezawodny dostęp do popularnych otwartych modeli do kodowania z hojnymi limitami użycia.",
"workspace.lite.promo.modelsTitle": "Co zawiera",
"workspace.lite.promo.footer":
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.",
"workspace.lite.promo.subscribe": "Subskrybuj Go",
"Uzyskaj dostęp do najlepszych otwartych modeli — Kimi K2.5, GLM-5 i MiniMax M2.5 — z hojnymi limitami użycia za $10 miesięcznie.",
"workspace.lite.promo.subscribe": "Subskrybuj Lite",
"workspace.lite.promo.subscribing": "Przekierowywanie...",
"download.title": "OpenCode | Pobierz",

View File

@@ -353,8 +353,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Запись кэша",
"workspace.usage.breakdown.output": "Выход",
"workspace.usage.breakdown.reasoning": "Reasoning (рассуждения)",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "подписка (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Расходы",
@@ -501,26 +501,21 @@ export const dict = {
"workspace.lite.time.minute": "минута",
"workspace.lite.time.minutes": "минут",
"workspace.lite.time.fewSeconds": "несколько секунд",
"workspace.lite.subscription.title": "Подписка Go",
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.",
"workspace.lite.subscription.title": "Подписка Lite",
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Lite.",
"workspace.lite.subscription.manage": "Управление подпиской",
"workspace.lite.subscription.rollingUsage": "Скользящее использование",
"workspace.lite.subscription.weeklyUsage": "Недельное использование",
"workspace.lite.subscription.monthlyUsage": "Ежемесячное использование",
"workspace.lite.subscription.resetsIn": "Сброс через",
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
"workspace.lite.other.title": "Подписка Go",
"workspace.lite.other.title": "Подписка Lite",
"workspace.lite.other.message":
"Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.",
"workspace.lite.promo.title": "OpenCode Go",
"Другой участник в этом рабочем пространстве уже подписан на OpenCode Lite. Только один участник в рабочем пространстве может оформить подписку.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go — это подписка за $10 в месяц, которая предоставляет надежный доступ к популярным открытым моделям для кодинга с щедрыми лимитами использования.",
"workspace.lite.promo.modelsTitle": "Что включено",
"workspace.lite.promo.footer":
"План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.",
"workspace.lite.promo.subscribe": "Подписаться на Go",
"Получите доступ к лучшим открытым моделям — Kimi K2.5, GLM-5 и MiniMax M2.5 — с щедрыми лимитами использования за $10 в месяц.",
"workspace.lite.promo.subscribe": "Подписаться на Lite",
"workspace.lite.promo.subscribing": "Перенаправление...",
"download.title": "OpenCode | Скачать",

View File

@@ -346,8 +346,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Cache Write",
"workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "สมัครสมาชิก (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "ค่าใช้จ่าย",
@@ -494,26 +494,21 @@ export const dict = {
"workspace.lite.time.minute": "นาที",
"workspace.lite.time.minutes": "นาที",
"workspace.lite.time.fewSeconds": "ไม่กี่วินาที",
"workspace.lite.subscription.title": "การสมัครสมาชิก Go",
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว",
"workspace.lite.subscription.title": "การสมัครสมาชิก Lite",
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Lite แล้ว",
"workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก",
"workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน",
"workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์",
"workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน",
"workspace.lite.subscription.resetsIn": "รีเซ็ตใน",
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
"workspace.lite.other.title": "การสมัครสมาชิก Go",
"workspace.lite.other.title": "การสมัครสมาชิก Lite",
"workspace.lite.other.message":
"สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น",
"workspace.lite.promo.title": "OpenCode Go",
"สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Lite แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go เป็นการสมัครสมาชิกราคา 10 ดอลลาร์ต่อเดือน ที่ให้การเข้าถึงโมเดลโอเพนโค้ดดิงยอดนิยมได้อย่างเสถียร ด้วยขีดจำกัดการใช้งานที่ครอบคลุม",
"workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย",
"workspace.lite.promo.footer":
"แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ",
"workspace.lite.promo.subscribe": "สมัครสมาชิก Go",
"เข้าถึงโมเดลเปิดที่ดีที่สุด — Kimi K2.5, GLM-5 และ MiniMax M2.5 — พร้อมขีดจำกัดการใช้งานมากมายในราคา $10 ต่อเดือน",
"workspace.lite.promo.subscribe": "สมัครสมาชิก Lite",
"workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...",
"download.title": "OpenCode | ดาวน์โหลด",

View File

@@ -349,8 +349,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "Önbellek Yazma",
"workspace.usage.breakdown.output": ıkış",
"workspace.usage.breakdown.reasoning": "Muhakeme",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "abonelik (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "Maliyet",
@@ -497,26 +497,21 @@ export const dict = {
"workspace.lite.time.minute": "dakika",
"workspace.lite.time.minutes": "dakika",
"workspace.lite.time.fewSeconds": "birkaç saniye",
"workspace.lite.subscription.title": "Go Aboneliği",
"workspace.lite.subscription.message": "OpenCode Go abonesisiniz.",
"workspace.lite.subscription.title": "Lite Aboneliği",
"workspace.lite.subscription.message": "OpenCode Lite abonesisiniz.",
"workspace.lite.subscription.manage": "Aboneliği Yönet",
"workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım",
"workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım",
"workspace.lite.subscription.monthlyUsage": "Aylık Kullanım",
"workspace.lite.subscription.resetsIn": "Sıfırlama süresi",
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
"workspace.lite.other.title": "Go Aboneliği",
"workspace.lite.other.title": "Lite Aboneliği",
"workspace.lite.other.message":
"Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.",
"workspace.lite.promo.title": "OpenCode Go",
"Bu çalışma alanındaki başka bir üye zaten OpenCode Lite abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go, cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.",
"workspace.lite.promo.modelsTitle": "Neler Dahil",
"workspace.lite.promo.footer":
"Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır; modeller istikrarlı küresel erişim için ABD, AB ve Singapur'da barındırılmaktadır. Erken kullanımdan öğrendikçe ve geri bildirim topladıkça fiyatlandırma ve kullanım limitleri değişebilir.",
"workspace.lite.promo.subscribe": "Go'ya Abone Ol",
"Ayda $10 karşılığında cömert kullanım limitleriyle en iyi açık modellere — Kimi K2.5, GLM-5 ve MiniMax M2.5 — erişin.",
"workspace.lite.promo.subscribe": "Lite'a Abone Ol",
"workspace.lite.promo.subscribing": "Yönlendiriliyor...",
"download.title": "OpenCode | İndir",

View File

@@ -334,8 +334,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "缓存写入",
"workspace.usage.breakdown.output": "输出",
"workspace.usage.breakdown.reasoning": "推理",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "订阅 (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "成本",
@@ -481,25 +481,20 @@ export const dict = {
"workspace.lite.time.minute": "分钟",
"workspace.lite.time.minutes": "分钟",
"workspace.lite.time.fewSeconds": "几秒钟",
"workspace.lite.subscription.title": "Go 订阅",
"workspace.lite.subscription.message": "您已订阅 OpenCode Go。",
"workspace.lite.subscription.title": "Lite 订阅",
"workspace.lite.subscription.message": "您已订阅 OpenCode Lite。",
"workspace.lite.subscription.manage": "管理订阅",
"workspace.lite.subscription.rollingUsage": "滚动用量",
"workspace.lite.subscription.weeklyUsage": "每周用量",
"workspace.lite.subscription.monthlyUsage": "每月用量",
"workspace.lite.subscription.resetsIn": "重置于",
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.lite.subscription.selectProvider":
"在你的 opencode 配置中选择「OpenCode Go」作为提供商即可使用 Go 模型。",
"workspace.lite.other.title": "Go 订阅",
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.other.title": "Lite 订阅",
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Lite。每个工作区只有一名成员可以订阅。",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额。",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。",
"workspace.lite.promo.subscribe": "订阅 Go",
"每月仅需 $10 即可访问最优秀的开源模型 — Kimi K2.5, GLM-5, 和 MiniMax M2.5 — 并享受充裕的使用额。",
"workspace.lite.promo.subscribe": "订阅 Lite",
"workspace.lite.promo.subscribing": "正在重定向...",
"download.title": "OpenCode | 下载",

View File

@@ -334,8 +334,8 @@ export const dict = {
"workspace.usage.breakdown.cacheWrite": "快取寫入",
"workspace.usage.breakdown.output": "輸出",
"workspace.usage.breakdown.reasoning": "推理",
"workspace.usage.subscription": "Black (${{amount}})",
"workspace.usage.lite": "Go (${{amount}})",
"workspace.usage.subscription": "訂閱 (${{amount}})",
"workspace.usage.lite": "lite (${{amount}})",
"workspace.usage.byok": "BYOK (${{amount}})",
"workspace.cost.title": "成本",
@@ -481,25 +481,20 @@ export const dict = {
"workspace.lite.time.minute": "分鐘",
"workspace.lite.time.minutes": "分鐘",
"workspace.lite.time.fewSeconds": "幾秒",
"workspace.lite.subscription.title": "Go 訂閱",
"workspace.lite.subscription.message": "您已訂閱 OpenCode Go。",
"workspace.lite.subscription.title": "Lite 訂閱",
"workspace.lite.subscription.message": "您已訂閱 OpenCode Lite。",
"workspace.lite.subscription.manage": "管理訂閱",
"workspace.lite.subscription.rollingUsage": "滾動使用量",
"workspace.lite.subscription.weeklyUsage": "每週使用量",
"workspace.lite.subscription.monthlyUsage": "每月使用量",
"workspace.lite.subscription.resetsIn": "重置時間:",
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
"workspace.lite.subscription.selectProvider":
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商即可使用 Go 模型。",
"workspace.lite.other.title": "Go 訂閱",
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.other.title": "Lite 訂閱",
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Lite。每個工作區只能有一位成員訂閱。",
"workspace.lite.promo.title": "OpenCode Lite",
"workspace.lite.promo.description":
"OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。",
"workspace.lite.promo.subscribe": "訂閱 Go",
"每月只需 $10 即可使用最佳的開放模型 — Kimi K2.5、GLM-5 和 MiniMax M2.5 — 並享有慷慨的使用限制。",
"workspace.lite.promo.subscribe": "訂閱 Lite",
"workspace.lite.promo.subscribing": "重新導向中...",
"download.title": "OpenCode | 下載",

View File

@@ -21,7 +21,7 @@ export default function () {
<Show when={isBlack()}>
<BlackSection />
</Show>
<Show when={!isBlack()}>
<Show when={!isBlack() && sessionInfo()?.isBeta}>
<LiteSection />
</Show>
<BillingSection />

View File

@@ -140,22 +140,6 @@
}
}
[data-slot="beta-notice"] {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-surface);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: 1.5;
margin-top: var(--space-3);
a {
color: var(--color-accent);
text-decoration: none;
}
}
[data-slot="other-message"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
@@ -163,26 +147,12 @@
}
[data-slot="promo-description"] {
font-size: var(--font-size-md);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: 1.5;
margin-top: var(--space-2);
}
[data-slot="promo-models-title"] {
font-size: var(--font-size-md);
font-weight: 600;
margin-top: var(--space-4);
}
[data-slot="promo-models"] {
margin: var(--space-2) 0 0 var(--space-4);
padding: 0;
font-size: var(--font-size-md);
color: var(--color-text-secondary);
line-height: 1.4;
}
[data-slot="subscribe-button"] {
align-self: flex-start;
margin-top: var(--space-4);

View File

@@ -11,7 +11,6 @@ import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./lite-section.module.css"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
const queryLiteSubscription = query(async (workspaceID: string) => {
"use server"
@@ -136,7 +135,6 @@ const setLiteUseBalance = action(async (form: FormData) => {
export function LiteSection() {
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
const lite = createAsync(() => queryLiteSubscription(params.id!))
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
@@ -183,13 +181,6 @@ export function LiteSection() {
</button>
</div>
</div>
<div data-slot="beta-notice">
{i18n.t("workspace.lite.subscription.selectProvider")}{" "}
<a href={language.route("/docs/providers/#opencode-go")} target="_blank" rel="noopener noreferrer">
{i18n.t("common.learnMore")}
</a>
.
</div>
<div data-slot="usage">
<div data-slot="usage-item">
<div data-slot="usage-header">
@@ -261,13 +252,6 @@ export function LiteSection() {
<h2>{i18n.t("workspace.lite.promo.title")}</h2>
</div>
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.description")}</p>
<h3 data-slot="promo-models-title">{i18n.t("workspace.lite.promo.modelsTitle")}</h3>
<ul data-slot="promo-models">
<li>Kimi K2.5</li>
<li>GLM-5</li>
<li>MiniMax M2.5</li>
</ul>
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.footer")}</p>
<button
data-slot="subscribe-button"
data-color="primary"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.2.14",
"version": "1.2.11",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.2.14",
"version": "1.2.11",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.2.14",
"version": "1.2.11",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1988,7 +1988,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
"windows-core 0.62.2",
]
[[package]]
@@ -3136,8 +3136,7 @@ dependencies = [
"tracing-subscriber",
"uuid",
"webkit2gtk",
"windows-core 0.62.2",
"windows-sys 0.61.2",
"windows 0.62.2",
]
[[package]]

View File

@@ -55,8 +55,7 @@ tokio-stream = { version = "0.1.18", features = ["sync"] }
process-wrap = { version = "9.0.3", features = ["tokio1"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_Threading", "Win32_System_Registry"] }
windows-core = "0.62"
windows = { version = "0.62", features = ["Win32_System_Threading"] }
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"

View File

@@ -19,7 +19,7 @@ use tokio::{
use tokio_stream::wrappers::ReceiverStream;
use tracing::Instrument;
#[cfg(windows)]
use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
use crate::server::get_wsl_config;
@@ -32,7 +32,7 @@ struct WinCreationFlags;
#[cfg(windows)]
impl CommandWrapper for WinCreationFlags {
fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> {
command.creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED);
command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0);
Ok(())
}
}

View File

@@ -6,7 +6,6 @@ pub mod linux_display;
pub mod linux_windowing;
mod logging;
mod markdown;
mod os;
mod server;
mod window_customizer;
mod windows;
@@ -43,7 +42,7 @@ struct ServerReadyData {
url: String,
username: Option<String>,
password: Option<String>,
is_sidecar: bool,
is_sidecar: bool
}
#[derive(Clone, Copy, serde::Serialize, specta::Type, Debug)]
@@ -149,7 +148,7 @@ async fn await_initialization(
fn check_app_exists(app_name: &str) -> bool {
#[cfg(target_os = "windows")]
{
os::windows::check_windows_app(app_name)
check_windows_app(app_name)
}
#[cfg(target_os = "macos")]
@@ -163,12 +162,156 @@ fn check_app_exists(app_name: &str) -> bool {
}
}
#[cfg(target_os = "windows")]
fn check_windows_app(_app_name: &str) -> bool {
// Check if command exists in PATH, including .exe
return true;
}
#[cfg(target_os = "windows")]
fn resolve_windows_app_path(app_name: &str) -> Option<String> {
use std::path::{Path, PathBuf};
// Try to find the command using 'where'
let output = Command::new("where").arg(app_name).output().ok()?;
if !output.status.success() {
return None;
}
let paths = String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect::<Vec<_>>();
let has_ext = |path: &Path, ext: &str| {
path.extension()
.and_then(|v| v.to_str())
.map(|v| v.eq_ignore_ascii_case(ext))
.unwrap_or(false)
};
if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) {
return Some(path.to_string_lossy().to_string());
}
let resolve_cmd = |path: &Path| -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
for token in content.split('"') {
let lower = token.to_ascii_lowercase();
if !lower.contains(".exe") {
continue;
}
if let Some(index) = lower.find("%~dp0") {
let base = path.parent()?;
let suffix = &token[index + 5..];
let mut resolved = PathBuf::from(base);
for part in suffix.replace('/', "\\").split('\\') {
if part.is_empty() || part == "." {
continue;
}
if part == ".." {
let _ = resolved.pop();
continue;
}
resolved.push(part);
}
if resolved.exists() {
return Some(resolved.to_string_lossy().to_string());
}
}
let resolved = PathBuf::from(token);
if resolved.exists() {
return Some(resolved.to_string_lossy().to_string());
}
}
None
};
for path in &paths {
if has_ext(path, "cmd") || has_ext(path, "bat") {
if let Some(resolved) = resolve_cmd(path) {
return Some(resolved);
}
}
if path.extension().is_none() {
let cmd = path.with_extension("cmd");
if cmd.exists() {
if let Some(resolved) = resolve_cmd(&cmd) {
return Some(resolved);
}
}
let bat = path.with_extension("bat");
if bat.exists() {
if let Some(resolved) = resolve_cmd(&bat) {
return Some(resolved);
}
}
}
}
let key = app_name
.chars()
.filter(|v| v.is_ascii_alphanumeric())
.flat_map(|v| v.to_lowercase())
.collect::<String>();
if !key.is_empty() {
for path in &paths {
let dirs = [
path.parent(),
path.parent().and_then(|dir| dir.parent()),
path.parent()
.and_then(|dir| dir.parent())
.and_then(|dir| dir.parent()),
];
for dir in dirs.into_iter().flatten() {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let candidate = entry.path();
if !has_ext(&candidate, "exe") {
continue;
}
let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else {
continue;
};
let name = stem
.chars()
.filter(|v| v.is_ascii_alphanumeric())
.flat_map(|v| v.to_lowercase())
.collect::<String>();
if name.contains(&key) || key.contains(&name) {
return Some(candidate.to_string_lossy().to_string());
}
}
}
}
}
}
paths.first().map(|path| path.to_string_lossy().to_string())
}
#[tauri::command]
#[specta::specta]
fn resolve_app_path(app_name: &str) -> Option<String> {
#[cfg(target_os = "windows")]
{
os::windows::resolve_windows_app_path(app_name)
resolve_windows_app_path(app_name)
}
#[cfg(not(target_os = "windows"))]
@@ -491,12 +634,7 @@ async fn initialize(app: AppHandle) {
app.state::<ServerState>().set_child(Some(child));
Ok(ServerReadyData {
url,
username,
password,
is_sidecar: true,
})
Ok(ServerReadyData { url, username,password, is_sidecar: true })
}
.map(move |res| {
let _ = server_ready_tx.send(res);

View File

@@ -1,2 +0,0 @@
#[cfg(windows)]
pub mod windows;

View File

@@ -1,439 +0,0 @@
use std::{
ffi::c_void,
os::windows::process::CommandExt,
path::{Path, PathBuf},
process::Command,
};
use windows_sys::Win32::{
Foundation::ERROR_SUCCESS,
System::Registry::{
HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, REG_EXPAND_SZ, REG_SZ, RRF_RT_REG_EXPAND_SZ,
RRF_RT_REG_SZ, RegGetValueW,
},
};
pub fn check_windows_app(app_name: &str) -> bool {
resolve_windows_app_path(app_name).is_some()
}
pub fn resolve_windows_app_path(app_name: &str) -> Option<String> {
fn expand_env(value: &str) -> String {
let mut out = String::with_capacity(value.len());
let mut index = 0;
while let Some(start) = value[index..].find('%') {
let start = index + start;
out.push_str(&value[index..start]);
let Some(end_rel) = value[start + 1..].find('%') else {
out.push_str(&value[start..]);
return out;
};
let end = start + 1 + end_rel;
let key = &value[start + 1..end];
if key.is_empty() {
out.push('%');
index = end + 1;
continue;
}
if let Ok(v) = std::env::var(key) {
out.push_str(&v);
index = end + 1;
continue;
}
out.push_str(&value[start..=end]);
index = end + 1;
}
out.push_str(&value[index..]);
out
}
fn extract_exe(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
return None;
}
if let Some(rest) = value.strip_prefix('"') {
if let Some(end) = rest.find('"') {
let inner = rest[..end].trim();
if inner.to_ascii_lowercase().contains(".exe") {
return Some(inner.to_string());
}
}
}
let lower = value.to_ascii_lowercase();
let end = lower.find(".exe")?;
Some(value[..end + 4].trim().trim_matches('"').to_string())
}
fn candidates(app_name: &str) -> Vec<String> {
let app_name = app_name.trim().trim_matches('"');
if app_name.is_empty() {
return vec![];
}
let mut out = Vec::<String>::new();
let mut push = |value: String| {
let value = value.trim().trim_matches('"').to_string();
if value.is_empty() {
return;
}
if out.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
return;
}
out.push(value);
};
push(app_name.to_string());
let lower = app_name.to_ascii_lowercase();
if !lower.ends_with(".exe") {
push(format!("{app_name}.exe"));
}
let snake = {
let mut s = String::new();
let mut underscore = false;
for c in lower.chars() {
if c.is_ascii_alphanumeric() {
s.push(c);
underscore = false;
continue;
}
if underscore {
continue;
}
s.push('_');
underscore = true;
}
s.trim_matches('_').to_string()
};
if !snake.is_empty() {
push(snake.clone());
if !snake.ends_with(".exe") {
push(format!("{snake}.exe"));
}
}
let alnum = lower
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>();
if !alnum.is_empty() {
push(alnum.clone());
push(format!("{alnum}.exe"));
}
match lower.as_str() {
"sublime text" | "sublime-text" | "sublime_text" | "sublime text.exe" => {
push("subl".to_string());
push("subl.exe".to_string());
push("sublime_text".to_string());
push("sublime_text.exe".to_string());
}
_ => {}
}
out
}
fn reg_app_path(exe: &str) -> Option<String> {
let exe = exe.trim().trim_matches('"');
if exe.is_empty() {
return None;
}
let query = |root: *mut c_void, subkey: &str| -> Option<String> {
let flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ;
let mut kind: u32 = 0;
let mut size = 0u32;
let mut key = subkey.encode_utf16().collect::<Vec<_>>();
key.push(0);
let status = unsafe {
RegGetValueW(
root,
key.as_ptr(),
std::ptr::null(),
flags,
&mut kind,
std::ptr::null_mut(),
&mut size,
)
};
if status != ERROR_SUCCESS || size == 0 {
return None;
}
if kind != REG_SZ && kind != REG_EXPAND_SZ {
return None;
}
let mut data = vec![0u8; size as usize];
let status = unsafe {
RegGetValueW(
root,
key.as_ptr(),
std::ptr::null(),
flags,
&mut kind,
data.as_mut_ptr() as *mut c_void,
&mut size,
)
};
if status != ERROR_SUCCESS || size < 2 {
return None;
}
let words = unsafe {
std::slice::from_raw_parts(data.as_ptr().cast::<u16>(), (size as usize) / 2)
};
let len = words.iter().position(|v| *v == 0).unwrap_or(words.len());
let value = String::from_utf16_lossy(&words[..len]).trim().to_string();
if value.is_empty() {
return None;
}
Some(value)
};
let keys = [
(
HKEY_CURRENT_USER,
format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
),
(
HKEY_LOCAL_MACHINE,
format!(r"Software\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
),
(
HKEY_LOCAL_MACHINE,
format!(r"Software\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\{exe}"),
),
];
for (root, key) in keys {
let Some(value) = query(root, &key) else {
continue;
};
let Some(exe) = extract_exe(&value) else {
continue;
};
let exe = expand_env(&exe);
let path = Path::new(exe.trim().trim_matches('"'));
if path.exists() {
return Some(path.to_string_lossy().to_string());
}
}
None
}
let app_name = app_name.trim().trim_matches('"');
if app_name.is_empty() {
return None;
}
let direct = Path::new(app_name);
if direct.is_absolute() && direct.exists() {
return Some(direct.to_string_lossy().to_string());
}
let key = app_name
.chars()
.filter(|v| v.is_ascii_alphanumeric())
.flat_map(|v| v.to_lowercase())
.collect::<String>();
let has_ext = |path: &Path, ext: &str| {
path.extension()
.and_then(|v| v.to_str())
.map(|v| v.eq_ignore_ascii_case(ext))
.unwrap_or(false)
};
let resolve_cmd = |path: &Path| -> Option<String> {
let bytes = std::fs::read(path).ok()?;
let content = String::from_utf8_lossy(&bytes);
for token in content.split('"') {
let Some(exe) = extract_exe(token) else {
continue;
};
let lower = exe.to_ascii_lowercase();
if let Some(index) = lower.find("%~dp0") {
let base = path.parent()?;
let suffix = &exe[index + 5..];
let mut resolved = PathBuf::from(base);
for part in suffix.replace('/', "\\").split('\\') {
if part.is_empty() || part == "." {
continue;
}
if part == ".." {
let _ = resolved.pop();
continue;
}
resolved.push(part);
}
if resolved.exists() {
return Some(resolved.to_string_lossy().to_string());
}
continue;
}
let resolved = PathBuf::from(expand_env(&exe));
if resolved.exists() {
return Some(resolved.to_string_lossy().to_string());
}
}
None
};
let resolve_where = |query: &str| -> Option<String> {
let output = Command::new("where")
.creation_flags(0x08000000)
.arg(query)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let paths = String::from_utf8_lossy(&output.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect::<Vec<_>>();
if paths.is_empty() {
return None;
}
if let Some(path) = paths.iter().find(|path| has_ext(path, "exe")) {
return Some(path.to_string_lossy().to_string());
}
for path in &paths {
if has_ext(path, "cmd") || has_ext(path, "bat") {
if let Some(resolved) = resolve_cmd(path) {
return Some(resolved);
}
}
if path.extension().is_none() {
let cmd = path.with_extension("cmd");
if cmd.exists() {
if let Some(resolved) = resolve_cmd(&cmd) {
return Some(resolved);
}
}
let bat = path.with_extension("bat");
if bat.exists() {
if let Some(resolved) = resolve_cmd(&bat) {
return Some(resolved);
}
}
}
}
if !key.is_empty() {
for path in &paths {
let dirs = [
path.parent(),
path.parent().and_then(|dir| dir.parent()),
path.parent()
.and_then(|dir| dir.parent())
.and_then(|dir| dir.parent()),
];
for dir in dirs.into_iter().flatten() {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let candidate = entry.path();
if !has_ext(&candidate, "exe") {
continue;
}
let Some(stem) = candidate.file_stem().and_then(|v| v.to_str()) else {
continue;
};
let name = stem
.chars()
.filter(|v| v.is_ascii_alphanumeric())
.flat_map(|v| v.to_lowercase())
.collect::<String>();
if name.contains(&key) || key.contains(&name) {
return Some(candidate.to_string_lossy().to_string());
}
}
}
}
}
}
paths.first().map(|path| path.to_string_lossy().to_string())
};
let list = candidates(app_name);
for query in &list {
if let Some(path) = resolve_where(query) {
return Some(path);
}
}
let mut exes = Vec::<String>::new();
for query in &list {
let query = query.trim().trim_matches('"');
if query.is_empty() {
continue;
}
let name = Path::new(query)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or(query);
let exe = if name.to_ascii_lowercase().ends_with(".exe") {
name.to_string()
} else {
format!("{name}.exe")
};
if exes.iter().any(|v| v.eq_ignore_ascii_case(&exe)) {
continue;
}
exes.push(exe);
}
for exe in exes {
if let Some(path) = reg_app_path(&exe) {
return Some(path);
}
}
None
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.2.14",
"version": "1.2.11",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.2.14"
version = "1.2.11"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.14/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.11/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.2.14",
"version": "1.2.11",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,136 +0,0 @@
# Bun shell migration plan
Practical phased replacement of Bun `$` calls.
## Goal
Replace runtime Bun shell template-tag usage in `packages/opencode/src` with a unified `Process` API in `util/process.ts`.
Keep behavior stable while improving safety, testability, and observability.
Current baseline from audit:
- 143 runtime command invocations across 17 files
- 84 are git commands
- Largest hotspots:
- `src/cli/cmd/github.ts` (33)
- `src/worktree/index.ts` (22)
- `src/lsp/server.ts` (21)
- `src/installation/index.ts` (20)
- `src/snapshot/index.ts` (18)
## Decisions
- Extend `src/util/process.ts` (do not create a separate exec module).
- Proceed with phased migration for both git and non-git paths.
- Keep plugin `$` compatibility in 1.x and remove in 2.0.
## Non-goals
- Do not remove plugin `$` compatibility in this effort.
- Do not redesign command semantics beyond what is needed to preserve behavior.
## Constraints
- Keep migration phased, not big-bang.
- Minimize behavioral drift.
- Keep these explicit shell-only exceptions:
- `src/session/prompt.ts` raw command execution
- worktree start scripts in `src/worktree/index.ts`
## Process API proposal (`src/util/process.ts`)
Add higher-level wrappers on top of current spawn support.
Core methods:
- `Process.run(cmd, opts)`
- `Process.text(cmd, opts)`
- `Process.lines(cmd, opts)`
- `Process.status(cmd, opts)`
- `Process.shell(command, opts)` for intentional shell execution
Git helpers:
- `Process.git(args, opts)`
- `Process.gitText(args, opts)`
Shared options:
- `cwd`, `env`, `stdin`, `stdout`, `stderr`, `abort`, `timeout`, `kill`
- `allowFailure` / non-throw mode
- optional redaction + trace metadata
Standard result shape:
- `code`, `stdout`, `stderr`, `duration_ms`, `cmd`
- helpers like `text()` and `arrayBuffer()` where useful
## Phased rollout
### Phase 0: Foundation
- Implement Process wrappers in `src/util/process.ts`.
- Refactor `src/util/git.ts` to use Process only.
- Add tests for exit handling, timeout, abort, and output capture.
### Phase 1: High-impact hotspots
Migrate these first:
- `src/cli/cmd/github.ts`
- `src/worktree/index.ts`
- `src/lsp/server.ts`
- `src/installation/index.ts`
- `src/snapshot/index.ts`
Within each file, migrate git paths first where applicable.
### Phase 2: Remaining git-heavy files
Migrate git-centric call sites to `Process.git*` helpers:
- `src/file/index.ts`
- `src/project/vcs.ts`
- `src/file/watcher.ts`
- `src/storage/storage.ts`
- `src/cli/cmd/pr.ts`
### Phase 3: Remaining non-git files
Migrate residual non-git usages:
- `src/cli/cmd/tui/util/clipboard.ts`
- `src/util/archive.ts`
- `src/file/ripgrep.ts`
- `src/tool/bash.ts`
- `src/cli/cmd/uninstall.ts`
### Phase 4: Stabilize
- Remove dead wrappers and one-off patterns.
- Keep plugin `$` compatibility isolated and documented as temporary.
- Create linked 2.0 task for plugin `$` removal.
## Validation strategy
- Unit tests for new `Process` methods and options.
- Integration tests on hotspot modules.
- Smoke tests for install, snapshot, worktree, and GitHub flows.
- Regression checks for output parsing behavior.
## Risk mitigation
- File-by-file PRs with small diffs.
- Preserve behavior first, simplify second.
- Keep shell-only exceptions explicit and documented.
- Add consistent error shaping and logging at Process layer.
## Definition of done
- Runtime Bun `$` usage in `packages/opencode/src` is removed except:
- approved shell-only exceptions
- temporary plugin compatibility path (1.x)
- Git paths use `Process.git*` consistently.
- CI and targeted smoke tests pass.
- 2.0 issue exists for plugin `$` removal.

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.2.14",
"version": "1.2.11",
"name": "opencode",
"type": "module",
"license": "MIT",

View File

@@ -13,6 +13,7 @@ import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process"
import { text } from "node:stream/consumers"
import { setTimeout as sleep } from "node:timers/promises"
type PluginAuth = NonNullable<Hooks["auth"]>
@@ -38,7 +39,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
const method = plugin.auth.methods[index]
// Handle prompts for all auth types
await Bun.sleep(10)
await sleep(10)
const inputs: Record<string, string> = {}
if (method.prompts) {
for (const prompt of method.prompts) {
@@ -268,17 +269,18 @@ export const AuthLoginCommand = cmd({
const proc = Process.spawn(wellknown.auth.command, {
stdout: "pipe",
})
if (!proc.stdout) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
if (!proc.stdout) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await text(proc.stdout)
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,

View File

@@ -3,6 +3,7 @@ import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { Log } from "../../../util/log"
import { EOL } from "os"
import { setTimeout as sleep } from "node:timers/promises"
export const LSPCommand = cmd({
command: "lsp",
@@ -19,7 +20,7 @@ const DiagnosticsCommand = cmd({
async handler(args) {
await bootstrap(process.cwd(), async () => {
await LSP.touchFile(args.file, true)
await Bun.sleep(1000)
await sleep(1000)
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
})
},

View File

@@ -28,6 +28,7 @@ import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt"
import { $ } from "bun"
import { setTimeout as sleep } from "node:timers/promises"
type GitHubAuthor = {
login: string
@@ -353,7 +354,7 @@ export const GithubInstallCommand = cmd({
}
retries++
await Bun.sleep(1000)
await sleep(1000)
} while (true)
s.stop("Installed GitHub app")
@@ -1372,7 +1373,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
} catch (e) {
if (retries > 0) {
console.log(`Retrying after ${delayMs}ms...`)
await Bun.sleep(delayMs)
await sleep(delayMs)
return withRetry(fn, retries - 1, delayMs)
}
throw e

View File

@@ -16,11 +16,10 @@ import { useToast } from "../ui/toast"
const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
openai: 1,
anthropic: 1,
"github-copilot": 2,
"opencode-go": 3,
anthropic: 4,
google: 5,
openai: 3,
google: 4,
}
export function createDialogProviderOptions() {
@@ -38,7 +37,6 @@ export function createDialogProviderOptions() {
opencode: "(Recommended)",
anthropic: "(Claude Max or API key)",
openai: "(ChatGPT Plus/Pro or API key)",
"opencode-go": "(Low cost)",
}[provider.id],
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
async onSelect() {
@@ -216,30 +214,16 @@ function ApiMethod(props: ApiMethodProps) {
title={props.title}
placeholder="API key"
description={
{
opencode: (
<box gap={1}>
<text fg={theme.textMuted}>
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API
key.
</text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
</text>
</box>
),
"opencode-go": (
<box gap={1}>
<text fg={theme.textMuted}>
OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models
with generous usage limits.
</text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> and enable OpenCode Go
</text>
</box>
),
}[props.providerID] ?? undefined
props.providerID === "opencode" ? (
<box gap={1}>
<text fg={theme.textMuted}>
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
</text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
</text>
</box>
) : undefined
}
onConfirm={async (value) => {
if (!value) return

View File

@@ -10,6 +10,7 @@ import { GlobalBus } from "@/bus/global"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import type { BunWebSocketData } from "hono/bun"
import { Flag } from "@/flag/flag"
import { setTimeout as sleep } from "node:timers/promises"
await Log.init({
print: process.argv.includes("--print-logs"),
@@ -75,7 +76,7 @@ const startEventStream = (directory: string) => {
).catch(() => undefined)
if (!events) {
await Bun.sleep(250)
await sleep(250)
continue
}
@@ -84,7 +85,7 @@ const startEventStream = (directory: string) => {
}
if (!signal.aborted) {
await Bun.sleep(250)
await sleep(250)
}
}
})().catch((error) => {

View File

@@ -4,6 +4,7 @@ import { Installation } from "../installation"
import { Auth, OAUTH_DUMMY_KEY } from "../auth"
import os from "os"
import { ProviderTransform } from "@/provider/transform"
import { setTimeout as sleep } from "node:timers/promises"
const log = Log.create({ service: "plugin.codex" })
@@ -602,7 +603,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
return { type: "failed" as const }
}
await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
await sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
}
},
}

View File

@@ -1,6 +1,7 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { Installation } from "@/installation"
import { iife } from "@/util/iife"
import { setTimeout as sleep } from "node:timers/promises"
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
// Add a small safety buffer when polling to avoid hitting the server
@@ -270,7 +271,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
}
if (data.error === "authorization_pending") {
await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
@@ -286,13 +287,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
newInterval = serverInterval * 1000
}
await Bun.sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS)
await sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
if (data.error) return { type: "failed" as const }
await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
},

View File

@@ -618,42 +618,6 @@ export const SessionRoutes = lazy(() =>
return c.json(message)
},
)
.delete(
"/:sessionID/message/:messageID",
describeRoute({
summary: "Delete message",
description:
"Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.",
operationId: "session.deleteMessage",
responses: {
200: {
description: "Successfully deleted message",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
sessionID: z.string().meta({ description: "Session ID" }),
messageID: z.string().meta({ description: "Message ID" }),
}),
),
async (c) => {
const params = c.req.valid("param")
SessionPrompt.assertNotBusy(params.sessionID)
await Session.removeMessage({
sessionID: params.sessionID,
messageID: params.messageID,
})
return c.json(true)
},
)
.delete(
"/:sessionID/message/:messageID/part/:partID",
describeRoute({

View File

@@ -697,9 +697,7 @@ export namespace Session {
async (input) => {
// CASCADE delete handles parts automatically
Database.use((db) => {
db.delete(MessageTable)
.where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID)))
.run()
db.delete(MessageTable).where(eq(MessageTable.id, input.messageID)).run()
Database.effect(() =>
Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID,
@@ -719,9 +717,7 @@ export namespace Session {
}),
async (input) => {
Database.use((db) => {
db.delete(PartTable)
.where(and(eq(PartTable.id, input.partID), eq(PartTable.session_id, input.sessionID)))
.run()
db.delete(PartTable).where(eq(PartTable.id, input.partID)).run()
Database.effect(() =>
Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,

View File

@@ -3,6 +3,7 @@ import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem"
import path from "path"
import { spawn, type ChildProcess } from "child_process"
import { setTimeout as sleep } from "node:timers/promises"
const SIGKILL_TIMEOUT_MS = 200
@@ -22,13 +23,13 @@ export namespace Shell {
try {
process.kill(-pid, "SIGTERM")
await Bun.sleep(SIGKILL_TIMEOUT_MS)
await sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
process.kill(-pid, "SIGKILL")
}
} catch (_e) {
proc.kill("SIGTERM")
await Bun.sleep(SIGKILL_TIMEOUT_MS)
await sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
proc.kill("SIGKILL")
}

View File

@@ -1,35 +1,63 @@
import { $ } from "bun"
import { buffer } from "node:stream/consumers"
import { Flag } from "../flag/flag"
import { Process } from "./process"
export interface GitResult {
exitCode: number
text(): string
stdout: Buffer
stderr: Buffer
text(): string | Promise<string>
stdout: Buffer | ReadableStream<Uint8Array>
stderr: Buffer | ReadableStream<Uint8Array>
}
/**
* Run a git command.
*
* Uses Process helpers with stdin ignored to avoid protocol pipe inheritance
* issues in embedded/client environments.
* Uses Bun's lightweight `$` shell by default. When the process is running
* as an ACP client, child processes inherit the parent's stdin pipe which
* carries protocol data on Windows this causes git to deadlock. In that
* case we fall back to `Process.spawn` with `stdin: "ignore"`.
*/
export async function git(args: string[], opts: { cwd: string; env?: Record<string, string> }): Promise<GitResult> {
return Process.run(["git", ...args], {
cwd: opts.cwd,
env: opts.env,
stdin: "ignore",
nothrow: true,
})
.then((result) => ({
exitCode: result.code,
text: () => result.stdout.toString(),
stdout: result.stdout,
stderr: result.stderr,
}))
.catch((error) => ({
exitCode: 1,
text: () => "",
stdout: Buffer.alloc(0),
stderr: Buffer.from(error instanceof Error ? error.message : String(error)),
}))
if (Flag.OPENCODE_CLIENT === "acp") {
try {
const proc = Process.spawn(["git", ...args], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
cwd: opts.cwd,
env: opts.env ? { ...process.env, ...opts.env } : process.env,
})
// Read output concurrently with exit to avoid pipe buffer deadlock
if (!proc.stdout || !proc.stderr) {
throw new Error("Process output not available")
}
const [exitCode, out, err] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
return {
exitCode,
text: () => out.toString(),
stdout: out,
stderr: err,
}
} catch (error) {
const stderr = Buffer.from(error instanceof Error ? error.message : String(error))
return {
exitCode: 1,
text: () => "",
stdout: Buffer.alloc(0),
stderr,
}
}
}
const env = opts.env ? { ...process.env, ...opts.env } : undefined
let cmd = $`git ${args}`.quiet().nothrow().cwd(opts.cwd)
if (env) cmd = cmd.env(env)
const result = await cmd
return {
exitCode: result.exitCode,
text: () => result.text(),
stdout: result.stdout,
stderr: result.stderr,
}
}

View File

@@ -1,5 +1,4 @@
import { spawn as launch, type ChildProcess } from "child_process"
import { buffer } from "node:stream/consumers"
export namespace Process {
export type Stdio = "inherit" | "pipe" | "ignore"
@@ -15,112 +14,58 @@ export namespace Process {
timeout?: number
}
export interface RunOptions extends Omit<Options, "stdout" | "stderr"> {
nothrow?: boolean
}
export interface Result {
code: number
stdout: Buffer
stderr: Buffer
}
export class RunFailedError extends Error {
readonly cmd: string[]
readonly code: number
readonly stdout: Buffer
readonly stderr: Buffer
constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) {
const text = stderr.toString().trim()
super(
text
? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}`
: `Command failed with code ${code}: ${cmd.join(" ")}`,
)
this.name = "ProcessRunFailedError"
this.cmd = [...cmd]
this.code = code
this.stdout = stdout
this.stderr = stderr
}
}
export type Child = ChildProcess & { exited: Promise<number> }
export function spawn(cmd: string[], opts: Options = {}): Child {
export function spawn(cmd: string[], options: Options = {}): Child {
if (cmd.length === 0) throw new Error("Command is required")
opts.abort?.throwIfAborted()
options.abort?.throwIfAborted()
const proc = launch(cmd[0], cmd.slice(1), {
cwd: opts.cwd,
env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
cwd: options.cwd,
env: options.env === null ? {} : options.env ? { ...process.env, ...options.env } : undefined,
stdio: [options.stdin ?? "ignore", options.stdout ?? "ignore", options.stderr ?? "ignore"],
})
let closed = false
let aborted = false
let timer: ReturnType<typeof setTimeout> | undefined
const abort = () => {
if (closed) return
if (aborted) return
if (proc.exitCode !== null || proc.signalCode !== null) return
closed = true
aborted = true
proc.kill(opts.kill ?? "SIGTERM")
proc.kill(options.kill ?? "SIGTERM")
const ms = opts.timeout ?? 5_000
if (ms <= 0) return
timer = setTimeout(() => proc.kill("SIGKILL"), ms)
const timeout = options.timeout ?? 5_000
if (timeout <= 0) return
timer = setTimeout(() => {
proc.kill("SIGKILL")
}, timeout)
}
const exited = new Promise<number>((resolve, reject) => {
const done = () => {
opts.abort?.removeEventListener("abort", abort)
options.abort?.removeEventListener("abort", abort)
if (timer) clearTimeout(timer)
}
proc.once("exit", (code, signal) => {
proc.once("exit", (exitCode, signal) => {
done()
resolve(code ?? (signal ? 1 : 0))
resolve(exitCode ?? (signal ? 1 : 0))
})
proc.once("error", (error) => {
done()
reject(error)
})
})
if (opts.abort) {
opts.abort.addEventListener("abort", abort, { once: true })
if (opts.abort.aborted) abort()
if (options.abort) {
options.abort.addEventListener("abort", abort, { once: true })
if (options.abort.aborted) abort()
}
const child = proc as Child
child.exited = exited
return child
}
export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result> {
const proc = spawn(cmd, {
cwd: opts.cwd,
env: opts.env,
stdin: opts.stdin,
abort: opts.abort,
kill: opts.kill,
timeout: opts.timeout,
stdout: "pipe",
stderr: "pipe",
})
if (!proc.stdout || !proc.stderr) throw new Error("Process output not available")
const [code, stdout, stderr] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
const out = {
code,
stdout,
stderr,
}
if (out.code === 0 || opts.nothrow) return out
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
}
}

View File

@@ -3,6 +3,7 @@
import os from "os"
import path from "path"
import fs from "fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
import { afterAll } from "bun:test"
// Set XDG env vars FIRST, before any src/ imports
@@ -15,7 +16,7 @@ afterAll(async () => {
typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY"
const rm = async (left: number): Promise<void> => {
Bun.gc(true)
await Bun.sleep(100)
await sleep(100)
return fs.rm(dir, { recursive: true, force: true }).catch((error) => {
if (!busy(error)) throw error
if (left <= 1) throw error

View File

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Pty } from "../../src/pty"
import { tmpdir } from "../fixture/fixture"
import { setTimeout as sleep } from "node:timers/promises"
describe("pty", () => {
test("does not leak output when websocket objects are reused", async () => {
@@ -43,7 +44,7 @@ describe("pty", () => {
// Output from a must never show up in b.
Pty.write(a.id, "AAA\n")
await Bun.sleep(100)
await sleep(100)
expect(outB.join("")).not.toContain("AAA")
} finally {
@@ -88,7 +89,7 @@ describe("pty", () => {
}
Pty.write(a.id, "AAA\n")
await Bun.sleep(100)
await sleep(100)
expect(outB.join("")).not.toContain("AAA")
} finally {
@@ -132,7 +133,7 @@ describe("pty", () => {
}
Pty.write(a.id, "AAA\n")
await Bun.sleep(100)
await sleep(100)
expect(outB.join("")).not.toContain("AAA")
} finally {

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import type { NamedError } from "@opencode-ai/util/error"
import { APICallError } from "ai"
import { setTimeout as sleep } from "node:timers/promises"
import { SessionRetry } from "../../src/session/retry"
import { MessageV2 } from "../../src/session/message-v2"
@@ -135,7 +136,7 @@ describe("session.message-v2.fromError", () => {
new ReadableStream({
async pull(controller) {
controller.enqueue("Hello,")
await Bun.sleep(10000)
await sleep(10000)
controller.enqueue(" World!")
controller.close()
},

View File

@@ -1,59 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Process } from "../../src/util/process"
function node(script: string) {
return [process.execPath, "-e", script]
}
describe("util.process", () => {
test("captures stdout and stderr", async () => {
const out = await Process.run(node('process.stdout.write("out");process.stderr.write("err")'))
expect(out.code).toBe(0)
expect(out.stdout.toString()).toBe("out")
expect(out.stderr.toString()).toBe("err")
})
test("returns code when nothrow is enabled", async () => {
const out = await Process.run(node("process.exit(7)"), { nothrow: true })
expect(out.code).toBe(7)
})
test("throws RunFailedError on non-zero exit", async () => {
const err = await Process.run(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error)
expect(err).toBeInstanceOf(Process.RunFailedError)
if (!(err instanceof Process.RunFailedError)) throw err
expect(err.code).toBe(3)
expect(err.stderr.toString()).toBe("bad")
})
test("aborts a running process", async () => {
const abort = new AbortController()
const started = Date.now()
setTimeout(() => abort.abort(), 25)
const out = await Process.run(node("setInterval(() => {}, 1000)"), {
abort: abort.signal,
nothrow: true,
})
expect(out.code).not.toBe(0)
expect(Date.now() - started).toBeLessThan(1000)
}, 3000)
test("kills after timeout when process ignores terminate signal", async () => {
if (process.platform === "win32") return
const abort = new AbortController()
const started = Date.now()
setTimeout(() => abort.abort(), 25)
const out = await Process.run(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), {
abort: abort.signal,
nothrow: true,
timeout: 25,
})
expect(out.code).not.toBe(0)
expect(Date.now() - started).toBeLessThan(1000)
}, 3000)
})

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -107,8 +107,6 @@ import type {
SessionCreateErrors,
SessionCreateResponses,
SessionDeleteErrors,
SessionDeleteMessageErrors,
SessionDeleteMessageResponses,
SessionDeleteResponses,
SessionDiffResponses,
SessionForkResponses,
@@ -1563,42 +1561,6 @@ export class Session2 extends HeyApiClient {
})
}
/**
* Delete message
*
* Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.
*/
public deleteMessage<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
messageID: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "messageID" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).delete<
SessionDeleteMessageResponses,
SessionDeleteMessageErrors,
ThrowOnError
>({
url: "/session/{sessionID}/message/{messageID}",
...options,
...params,
})
}
/**
* Get message
*

View File

@@ -3564,46 +3564,6 @@ export type SessionPromptResponses = {
export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses]
export type SessionDeleteMessageData = {
body?: never
path: {
/**
* Session ID
*/
sessionID: string
/**
* Message ID
*/
messageID: string
}
query?: {
directory?: string
}
url: "/session/{sessionID}/message/{messageID}"
}
export type SessionDeleteMessageErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors]
export type SessionDeleteMessageResponses = {
/**
* Successfully deleted message
*/
200: boolean
}
export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses]
export type SessionMessageData = {
body?: never
path: {

View File

@@ -2630,76 +2630,6 @@
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.message({\n ...\n})"
}
]
},
"delete": {
"operationId": "session.deleteMessage",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "sessionID",
"schema": {
"type": "string"
},
"required": true,
"description": "Session ID"
},
{
"in": "path",
"name": "messageID",
"schema": {
"type": "string"
},
"required": true,
"description": "Message ID"
}
],
"summary": "Delete message",
"description": "Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.",
"responses": {
"200": {
"description": "Successfully deleted message",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.deleteMessage({\n ...\n})"
}
]
}
},
"/session/{sessionID}/message/{messageID}/part/{partID}": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.2.14",
"version": "1.2.11",
"type": "module",
"license": "MIT",
"exports": {

View File

@@ -252,12 +252,6 @@
}
}
@media (prefers-color-scheme: dark) {
[data-component="reasoning-part"] [data-component="markdown"] :not(pre) > code {
opacity: 0.6;
}
}
[data-component="tool-error"] {
display: flex;
align-items: start;

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.2.14",
"version": "1.2.11",
"private": true,
"type": "module",
"license": "MIT",

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