Compare commits

..
1 Commits
Author SHA1 Message Date
Aiden Cline ea8077a8f4 fix(cli): preserve attached run output 2026-09-02 15:32:57 -05:00
85 changed files with 415 additions and 3190 deletions
+31 -31
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.27",
"version": "1.18.26",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -195,7 +195,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -222,7 +222,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -245,7 +245,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -269,7 +269,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -289,7 +289,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.27",
"version": "1.18.26",
"bin": {
"opencode": "./bin/opencode",
},
@@ -341,7 +341,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.13.0",
"gitlab-ai-provider": "6.12.1",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -383,7 +383,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:",
@@ -437,7 +437,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -451,7 +451,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"effect": "catalog:",
},
@@ -463,7 +463,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -495,7 +495,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -511,7 +511,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -542,7 +542,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -561,7 +561,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.27",
"version": "1.18.26",
"bin": {
"opencode": "./bin/opencode",
},
@@ -634,7 +634,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.13.0",
"gitlab-ai-provider": "6.12.1",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -692,7 +692,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -768,7 +768,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -783,7 +783,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -798,7 +798,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -838,7 +838,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -851,7 +851,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -878,7 +878,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -897,7 +897,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -939,7 +939,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -966,7 +966,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1017,7 +1017,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -3845,7 +3845,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.13.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-JDZhNjvoiB7xBfesNegXNDg6ItKf9M2l8/DifuIyuvYmGgaZnDDbl89QrOKSoIzAOpo/+3Om0qviBPm84nGEbg=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-2kzFIn42mD7ZDu/+6lWctqjZ/lIVZZfjZhmF/ymhF54=",
"aarch64-linux": "sha256-4HlReGD3gYfyyfnY/FQ46Ov+g3ZGV3sBYQ9p1bS9YAY=",
"aarch64-darwin": "sha256-dkfCoH/sW9XBPZ7XhnUoE54TY1lcfNvZ5wQQLr7gKiQ=",
"x86_64-darwin": "sha256-32t1JEcibZ4OrornIgfWOGQA87FOXjrxrxlDrflh7Ss="
"x86_64-linux": "sha256-SVvFPO+KuS67+6XGPhaB3cIuc3XUyM0XVccy5v8afS4=",
"aarch64-linux": "sha256-HJRrSu5u0TEg214d2RAbM1C+nmrxvIR/d7JlSBOGb9I=",
"aarch64-darwin": "sha256-ytmHSdC0PVGAJSbpt/+YwL5ySF3w6r/9uZpZACPppkI=",
"x86_64-darwin": "sha256-ma7K4K+xn7Jz3+YPA/n8ly1o308UNoM/DO9Z8yZSAKE="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.27",
"version": "1.18.26",
"description": "",
"type": "module",
"exports": {
@@ -33,7 +33,6 @@ import { ExternalLink } from "@/components/external-link"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { CustomProviderForm } from "./dialog-custom-provider"
@@ -387,7 +386,6 @@ function ProviderConnection(props: {
const serverSDK = useServerSDK()
const params = useParams()
const language = useLanguage()
const platform = usePlatform()
const settings = useSettings()
const newLayout = settings.general.newLayoutDesigns
const providers = useProviders(() => props.directory?.())
@@ -562,11 +560,6 @@ function ProviderConnection(props: {
})
.then((x) => {
if (!alive.value) return
if (props.provider === "opencode" && platform.platform === "desktop") {
const url = new URL(x.data.url)
url.searchParams.set("client_id", "opencode-desktop")
x.data.url = url.href
}
dispatch({ type: "auth.complete", authorization: x.data })
})
.catch((e) => {
@@ -31,10 +31,7 @@ export function OpenInAppV2(props: { directory: () => string }) {
disabled={state.opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })}
>
<Show
when={state.opening()}
fallback={<AppIcon id={state.current().icon} style={{ width: "20px", height: "20px" }} />}
>
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
<Spinner class="size-3.5" />
</Show>
</SplitButtonV2Action>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.27",
"version": "1.18.26",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"scripts": {
@@ -4,7 +4,7 @@ import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
const paths: Record<string, string | undefined> = {
"GET /zen/v1/models": "/v1/models",
"GET /zen/v1/models": "/openai/v1/models",
"POST /zen/v1/chat/completions": "/openai/v1/chat/completions",
"POST /zen/v1/responses": "/openai/v1/responses",
"POST /zen/v1/messages": "/anthropic/v1/messages",
+2 -43
View File
@@ -1,9 +1,8 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource, waitUntil } from "@opencode-ai/console-resource"
import { Resource } from "@opencode-ai/console-resource"
import { LOCALE_HEADER, cookie, localeFromRequest, route, tag } from "~/lib/language"
const dataPath = "/data"
const statsCacheParam = "__opencode_stats_locale"
export async function statsProxy(evt: APIEvent) {
const req = evt.request.clone()
@@ -11,13 +10,6 @@ export async function statsProxy(evt: APIEvent) {
const redirect = redirectToLocalizedData(req, new URL(req.url), locale)
if (redirect) return redirect
const cache = defaultCache(caches)
const cacheKey = statsCacheKey(req, locale)
if (cacheKey) {
const cached = await cache.match(cacheKey)
if (cached) return withStatsCacheStatus(cached, "HIT")
}
const targetUrl = new URL(req.url)
targetUrl.protocol = "https:"
targetUrl.hostname = Resource.App.stage === "production" ? "stats.opencode.ai" : "stats.dev.opencode.ai"
@@ -48,18 +40,13 @@ export async function statsProxy(evt: APIEvent) {
headers.delete("content-encoding")
headers.delete("content-length")
headers.delete("etag")
headers.delete("set-cookie")
appendVary(headers, "Accept-Language", "Cookie", LOCALE_HEADER)
const result = new Response(rewriteStatsHtml(await response.text()), {
return new Response(rewriteStatsHtml(await response.text()), {
status: response.status,
statusText: response.statusText,
headers,
})
if (!cacheKey || !response.ok) return result
void waitUntil(cache.put(cacheKey, result.clone()))
return withStatsCacheStatus(result, "MISS")
}
export function statsRedirect(evt: APIEvent) {
@@ -77,34 +64,6 @@ function rewriteStatsHtml(html: string) {
return html.replaceAll('"/_build/', `"${dataPath}/_build/`).replaceAll("'/_build/", `'${dataPath}/_build/`)
}
function statsCacheKey(request: Request, locale: ReturnType<typeof localeFromRequest>): Request | undefined {
if (request.method !== "GET") return undefined
if (!acceptsHtml(request)) return undefined
if (isDataBypassPath(new URL(request.url).pathname)) return undefined
const url = new URL(request.url)
const additionalModels = url.searchParams.get("add")
url.search = ""
if (additionalModels) url.searchParams.set("add", additionalModels)
url.searchParams.set(statsCacheParam, locale)
return new Request(url)
}
function defaultCache(storage: CacheStorage) {
if (!isCloudflareCacheStorage(storage)) throw new Error("Cloudflare default cache is unavailable")
return storage.default
}
function isCloudflareCacheStorage(storage: CacheStorage): storage is CacheStorage & { default: Cache } {
return "default" in storage
}
function withStatsCacheStatus(response: Response, status: "HIT" | "MISS") {
const headers = new Headers(response.headers)
headers.set("x-opencode-stats-cache", status)
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
}
function redirectToLocalizedData(request: Request, url: URL, locale: ReturnType<typeof localeFromRequest>) {
if (locale === "en") return null
if (request.headers.get(LOCALE_HEADER)) return null
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.27",
"version": "1.18.26",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.18.27",
"version": "1.18.26",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"scripts": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
@@ -108,7 +108,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.13.0",
"gitlab-ai-provider": "6.12.1",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
+4 -9
View File
@@ -108,20 +108,15 @@ export const Info = Schema.Struct({
headerTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.",
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.",
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
chunkTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
}),
).annotate({
chunkTimeout: Schema.optional(PositiveInt).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
}),
}),
[Schema.Record(Schema.String, Schema.Any)],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.27",
"version": "1.18.26",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.27",
"version": "1.18.26",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -120,7 +120,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.13.0",
"gitlab-ai-provider": "6.12.1",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
+45 -7
View File
@@ -22,7 +22,7 @@ import { UI } from "../ui"
import { effectCmd } from "../effect-cmd"
import { EOL } from "os"
import { Filesystem } from "@/util/filesystem"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { createOpencodeClient, type Event, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { FormatError, FormatUnknownError } from "../error"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
@@ -690,12 +690,14 @@ export const RunCommand = effectCmd({
return false
}
const toggles = new Map<string, boolean>()
const printed = new Set<string>()
// Consume one subscribed event stream for the active session and mirror it
// to stdout/UI. `client` is passed explicitly because attach mode may
// rebind the SDK to the session's directory after the subscription is
// created, and replies issued from inside the loop must use that client.
async function loop(client: OpencodeClient, events: Awaited<ReturnType<typeof sdk.event.subscribe>>) {
const toggles = new Map<string, boolean>()
const sessions = new Set([sessionID])
let error: string | undefined
@@ -720,6 +722,16 @@ export const RunCommand = effectCmd({
if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.sessionID !== sessionID) continue
if (
args.attach &&
(part.type === "step-start" ||
part.type === "step-finish" ||
((part.type === "text" || part.type === "reasoning") && part.time?.end) ||
(part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")))
) {
if (printed.has(part.id)) continue
printed.add(part.id)
}
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
if (emit("tool_use", { part })) continue
@@ -831,15 +843,41 @@ export const RunCommand = effectCmd({
await share(client, sessionID)
if (!interactive) {
const events = await client.event.subscribe()
const controller = new AbortController()
const events = await client.event.subscribe(undefined, { signal: controller.signal })
const completed = loop(client, events).catch((e) => {
console.error(e)
process.exitCode = 1
})
async function finish() {
if (args.attach) return
async function finish(parentID?: string) {
if (args.attach) controller.abort()
const error = await completed
if (error) process.exitCode = 1
if (!args.attach || !parentID) return
// The request has completed, but remote events may still be buffered
// or missing. Recover this prompt's output without waiting for idle.
const messages = await client.session.messages({ sessionID }, { throwOnError: true })
async function* replay(): AsyncGenerator<Event> {
for (const message of messages.data) {
if (message.info.role !== "assistant" || message.info.parentID !== parentID) continue
yield { id: message.info.id, type: "message.updated", properties: { sessionID, info: message.info } }
for (const part of message.parts)
yield {
id: part.id,
type: "message.part.updated",
properties: { sessionID, part, time: message.info.time.completed ?? message.info.time.created },
}
if (message.info.error && !error)
yield {
id: message.info.id,
type: "session.error",
properties: { sessionID, error: message.info.error },
}
}
}
const replayError = await loop(client, { stream: replay() })
if (replayError) process.exitCode = 1
}
if (args.command) {
@@ -856,7 +894,7 @@ export const RunCommand = effectCmd({
process.exitCode = 1
return
}
await finish()
await finish(result.data?.info.parentID)
return
}
@@ -873,7 +911,7 @@ export const RunCommand = effectCmd({
process.exitCode = 1
return
}
await finish()
await finish(result.data?.info.parentID)
return
}
+2 -2
View File
@@ -1792,8 +1792,8 @@ const layer = Layer.effect(
if (existing) return existing
const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"] ?? 300_000
const headerTimeout = options["headerTimeout"] ?? 300_000
const chunkTimeout = options["chunkTimeout"]
const headerTimeout = options["headerTimeout"]
delete options["chunkTimeout"]
delete options["headerTimeout"]
@@ -7,6 +7,7 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { reply } from "../../lib/llm-server"
import { cliIt } from "../../lib/cli-process"
import { testProviderConfig } from "../../lib/test-provider"
describe("opencode run (non-interactive subprocess)", () => {
// Happy path: prompt completes, output reaches stdout, process exits 0.
@@ -299,6 +300,7 @@ describe("opencode run (non-interactive subprocess)", () => {
})
opencode.expectExit(result, 0)
expect(result.stdout).toBe("attachment received\n")
const input = JSON.stringify(yield* llm.inputs)
expect(input).toContain(sentinel)
expect(input).not.toContain(`file://${source}`)
@@ -306,6 +308,104 @@ describe("opencode run (non-interactive subprocess)", () => {
60_000,
)
for (const mode of ["stream", "missing-idle", "command"]) {
cliIt.live(
`attach preserves completed output (${mode})`,
({ llm, opencode }) =>
Effect.gen(function* () {
yield* llm.push(
reply().reason("thinking").text("before").tool("bash", {
command: "printf tool-output",
description: "Print deterministic output",
}),
)
yield* llm.text("after")
const server = yield* opencode.serve({
env: {
OPENCODE_CONFIG_CONTENT: JSON.stringify({
...testProviderConfig(llm.url),
command: { probe: { template: "exercise output" } },
}),
},
})
// Keep SSE connected but withhold its events, including session idle.
const proxy = yield* Effect.acquireRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/event" && mode !== "stream") {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"server.connected","properties":{}}\n\n'),
)
},
}),
{ headers: { "Content-Type": "text/event-stream" } },
)
}
const forwarded = new Request(new URL(url.pathname + url.search, server.url), request)
forwarded.headers.set("Accept-Encoding", "identity")
return fetch(forwarded)
},
}),
),
(proxy) => Effect.sync(() => proxy.stop(true)),
)
const result = yield* opencode.run("exercise output", {
command: mode === "command" ? "probe" : undefined,
format: "json",
extraArgs: ["--attach", proxy.url.origin, "--thinking", "--dangerously-skip-permissions"],
})
opencode.expectExit(result, 0)
const events = opencode.parseJsonEvents(result.stdout)
expect(events.map((event) => event.type)).toEqual([
"step_start",
"reasoning",
"text",
"tool_use",
"step_finish",
"step_start",
"text",
"step_finish",
])
expect(events.filter((event) => event.type === "text").map((event) => event.part)).toEqual([
expect.objectContaining({ text: "before" }),
expect.objectContaining({ text: "after" }),
])
yield* llm.text("next turn")
const resumed = yield* opencode.run("continue", {
format: "json",
extraArgs: ["--attach", proxy.url.origin, "--session", String(events[0].sessionID)],
})
opencode.expectExit(resumed, 0)
expect(opencode.parseJsonEvents(resumed.stdout).map((event) => event.type)).toEqual([
"step_start",
"text",
"step_finish",
])
expect(resumed.stdout).toContain("next turn")
expect(resumed.stdout).not.toContain('"before"')
const rejected = yield* opencode.run("unknown model", {
model: "test/nonexistent-model",
format: "json",
extraArgs: ["--attach", proxy.url.origin],
})
expect(rejected.exitCode).not.toBe(0)
const errors = opencode.parseJsonEvents(rejected.stdout)
expect(errors.length).toBeGreaterThan(0)
expect(errors.every((event) => event.type === "error")).toBe(true)
}),
60_000,
)
}
cliIt.concurrent(
"attach mode rejects local directories before prompt admission",
({ home, opencode }) =>
@@ -13,8 +13,6 @@ import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderError } from "@/provider/error"
import { MessageV2 } from "@/session/message-v2"
import { SessionRetry } from "@/session/retry"
afterEach(async () => {
await disposeAllInstances()
@@ -48,48 +46,7 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", ()
}),
)
for (const timeout of ["chunkTimeout", "headerTimeout"] as const) {
it.live(`default ${timeout} is applied at fetch without changing provider options`, () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
(server) => Effect.sync(() => server.server.close()),
)
yield* provideTmpdirInstance(
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const configured = yield* provider.getProvider(ProviderV2.ID.make("test"))
const signals: (AbortSignal | null | undefined)[] = []
configured.options.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
signals.push(init?.signal)
return fetch(input, init)
}
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
const language = yield* provider.getLanguage(model)
yield* Effect.acquireRelease(
Effect.promise(() =>
language.doStream({ prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }),
),
(result) => Effect.promise(() => result.stream.cancel()),
)
expect(signals).toHaveLength(1)
expect(signals[0]).toBeInstanceOf(AbortSignal)
expect(configured.options[timeout]).toBeUndefined()
}),
{
config: providerConfig(server.url, {
[timeout === "chunkTimeout" ? "headerTimeout" : "chunkTimeout"]: false,
}),
},
)
}),
)
}
it.live("configured chunkTimeout raises a retryable response stream error when SSE body stalls", () =>
it.live("chunkTimeout raises a response stream error when SSE body stalls", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
@@ -117,41 +74,12 @@ it.live("configured chunkTimeout raises a retryable response stream error when S
}
})
expect(error).toBeInstanceOf(ProviderError.ResponseStreamError)
expect(
SessionRetry.retryable(MessageV2.fromError(error, { providerID: model.providerID }), model.providerID),
).toEqual({ message: "SSE read timed out" })
}),
{ config: providerConfig(server.url, { chunkTimeout: 50 }) },
)
}),
)
it.live("chunkTimeout can be disabled with false", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
(server) => Effect.sync(() => server.server.close()),
)
yield* provideTmpdirInstance(
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const configured = yield* provider.getProvider(ProviderV2.ID.make("test"))
expect(configured.options.chunkTimeout).toBe(false)
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
messages: [{ role: "user", content: "hello" }],
})
expect(yield* Effect.promise(() => result.text)).toBe("late")
}),
{ config: providerConfig(server.url, { chunkTimeout: false }) },
)
}),
)
it.live("headerTimeout aborts when response headers do not arrive", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
@@ -184,7 +112,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () =>
}),
)
it.live("headerTimeout can be disabled with false for non-OpenAI providers", () =>
it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedHeaderServer(100)),
@@ -203,12 +131,12 @@ it.live("headerTimeout can be disabled with false for non-OpenAI providers", ()
expect(yield* Effect.promise(() => result.text)).toBe("ok")
}),
{ config: providerConfig(server.url, { headerTimeout: false }) },
{ config: providerConfig(server.url) },
)
}),
)
it.live("OpenAI Codex header and chunk timeout defaults can be disabled by config", () =>
it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
Effect.gen(function* () {
yield* withAuthContent(
Effect.gen(function* () {
@@ -218,9 +146,8 @@ it.live("OpenAI Codex header and chunk timeout defaults can be disabled by confi
const provider = yield* Provider.Service
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
expect(openai.options.headerTimeout).toBe(false)
expect(openai.options.chunkTimeout).toBe(false)
}),
{ config: { provider: { openai: { options: { headerTimeout: false, chunkTimeout: false } } } } },
{ config: { provider: { openai: { options: { headerTimeout: false } } } } },
)
}),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"scripts": {
+3 -6
View File
@@ -1751,14 +1751,11 @@ export type ProviderConfig = {
*/
timeout?: number | false
/**
* Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.
* Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.
*/
headerTimeout?: number | false
/**
* Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.
*/
chunkTimeout?: number | false
[key: string]: unknown | string | boolean | number | false | number | false | number | false | undefined
chunkTimeout?: number
[key: string]: unknown | string | boolean | number | false | number | false | number | undefined
}
models?: {
[key: string]: {
+3 -12
View File
@@ -20780,20 +20780,11 @@
"enum": [false]
}
],
"description": "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout."
"description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout."
},
"chunkTimeout": {
"anyOf": [
{
"type": "integer",
"exclusiveMinimum": 0
},
{
"type": "boolean",
"enum": [false]
}
],
"description": "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout."
"type": "integer",
"exclusiveMinimum": 0
}
},
"additionalProperties": {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
@@ -7,6 +7,7 @@ import {
type StatsModelComparisonInput,
type StatsModelComparisonEntry,
} from "@opencode-ai/stats-core/domain/home"
import { runtime } from "@opencode-ai/stats-core/runtime"
import { createAsync, query, useParams, useSearchParams } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
@@ -45,7 +46,6 @@ import {
type ResolvedComparisonFamily,
} from "../lib/comparison-pages"
import { baseUrl } from "../lib/language"
import { runStatsEffect } from "../stats-runtime"
const compareHeaderLinks: readonly HeaderLink[] = [
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
@@ -110,7 +110,7 @@ export type ModelCompareDetailPageProps = {
const getComparisonData = query(async (models: StatsModelComparisonInput[]) => {
"use server"
return runStatsEffect(getStatsModelsComparisonData(models))
return runtime.runPromise(getStatsModelsComparisonData(models))
}, "getStatsModelComparisonDetailData")
export default function ModelCompareDetailPage(props: ModelCompareDetailPageProps = {}) {
@@ -52,7 +52,7 @@ type ModelPageCatalog = {
labs: { id: string; name: string }[]
labModels: ModelCatalogOption[]
}
type StatsModelPageData = StatsModelData
type StatsModelPageData = Omit<StatsModelData, "country"> & { country: CountryEntry[] }
type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null }
const countryNumericIds = new Map(
@@ -75,7 +75,7 @@ const getModelPageData = query(async (labParam: string, modelParam: string) => {
.find((item) => item.id === (entry?.lab ?? providerSlug(labParam)))
?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [],
},
stats,
stats: stats ? { ...stats, country: stats.country["2M"] } : null,
} satisfies ModelPageData
}, "getStatsModelPageData")
-125
View File
@@ -2195,7 +2195,6 @@ body {
[data-page="stats"] [data-component="market-share"] {
--market-gap: 12px;
position: relative;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
@@ -2361,106 +2360,6 @@ body {
font-weight: 500;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] {
top: 52px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 0;
width: 228px;
min-width: 228px;
padding: 0;
border: 0;
background: #fffffff2;
box-shadow:
0 0 0 0.5px #00000024,
0 8px 16px #0000000f,
0 4px 8px #00000014;
color: var(--stats-text);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement="right"] {
right: auto;
left: calc(var(--market-tooltip-left) + 8px);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement="left"] {
right: calc(var(--market-tooltip-right) + 8px);
left: auto;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] strong,
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] > span {
display: block;
font-size: 11px;
line-height: 12px;
white-space: nowrap;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] strong {
padding: 8px 8px 0;
font-weight: 500;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] > span {
padding: 4px 8px 8px;
color: var(--stats-muted);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] [data-slot="tooltip-divider"] {
height: 0.5px;
margin: 0;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p {
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
height: 16px;
margin: 4px 0 0;
padding: 0 8px;
font-size: 11px;
font-weight: 500;
line-height: 12px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p[data-muted="true"] {
opacity: 0.46;
}
[data-page="stats"]
[data-component="market-share"]
> [data-component="chart-tooltip"]
[data-slot="tooltip-divider"]
+ p {
margin-top: 8px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p:last-child {
margin-bottom: 8px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] [data-slot="tooltip-label"] {
grid-template-columns: 16px minmax(0, 1fr);
gap: 4px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] i {
width: 6px;
height: 6px;
justify-self: center;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] em,
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] b {
font-style: normal;
font-weight: 500;
white-space: nowrap;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] em {
color: var(--stats-muted);
}
[data-page="stats"] [data-slot="market-footer"] {
display: flex;
align-items: center;
@@ -7309,11 +7208,6 @@ body {
[data-page="stats"][data-theme="dark"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-component="market-share"]
> [data-component="chart-tooltip"],
[data-page="stats"][data-theme="dark"] [data-component="market-share"] > [data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
:is([data-section="top-models"], [data-section="unique-users"])
@@ -8666,25 +8560,6 @@ body {
transform: none;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] {
position: fixed;
top: auto;
right: 12px;
bottom: 12px;
left: 12px;
z-index: 40;
width: auto;
min-width: 0;
max-height: min(320px, 48vh);
overflow: auto;
transform: none;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement] {
right: 12px;
left: 12px;
}
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"][data-placement] {
+32 -71
View File
@@ -91,7 +91,7 @@ const getData = query(async () => {
cacheRatio: stats.cacheRatio.Go,
sessionCost: stats.sessionCost.Go,
retention: stats.retention,
country: stats.country,
country: stats.country["2M"],
} satisfies StatsHomePageData
}, "getStatsHomeData")
@@ -928,7 +928,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
const [inspecting, setInspecting] = createSignal(false)
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
const today = createMemo(() => props.data[props.data.length - 1])
const activeDay = createMemo(() => props.data[selectedIndex()])
return (
<section
@@ -947,7 +947,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
description={i18n.t("home.marketShareDescription")}
/>
<Show
when={today()}
when={activeDay()}
fallback={<EmptyState title={i18n.t("home.noMarketTitle")} description={i18n.t("home.noMarketDescription")} />}
>
{(day) => (
@@ -963,14 +963,19 @@ function MarketShareSection(props: { data: MarketDay[] }) {
setActiveIndex(index)
setInspecting(true)
}}
onActiveAuthorChange={setActiveAuthor}
onInspectingChange={setInspecting}
onActiveAuthorChange={(author) => {
setActiveAuthor(author)
setInspecting(true)
}}
/>
<MarketShareList
data={rankedMarketAuthors(day())}
data={day().authors}
authorOrder={authorOrder()}
activeAuthor={activeAuthor()}
onActiveAuthorChange={setActiveAuthor}
onActiveAuthorChange={(author) => {
setActiveAuthor(author)
setInspecting(true)
}}
/>
</>
)}
@@ -978,7 +983,11 @@ function MarketShareSection(props: { data: MarketDay[] }) {
<div data-slot="market-footer">
<p>
<span>[*]</span>
<strong>{formatMarketDate(today(), i18n.t("home.noData"))}</strong>
<strong>
{inspecting()
? formatMarketDate(activeDay(), i18n.t("home.noData"))
: formatMarketRange(props.data, i18n.t("home.noData"))}
</strong>
</p>
</div>
</section>
@@ -993,15 +1002,10 @@ function MarketShare(props: {
activeAuthor: string | undefined
inspecting: boolean
onActiveIndexChange: (index: number) => void
onActiveAuthorChange: (author: string | undefined) => void
onInspectingChange: (inspecting: boolean) => void
onActiveAuthorChange: (author: string) => void
}) {
const i18n = useI18n()
let chartRef: HTMLDivElement | undefined
const inspectDay = (index: number) => {
props.onActiveIndexChange(index)
props.onActiveAuthorChange(undefined)
}
createEffect(() => scrollDenseChartToEnd(chartRef, props.range, props.data.length))
@@ -1014,10 +1018,6 @@ function MarketShare(props: {
role="img"
aria-label={i18n.t("home.marketChart")}
style={{ "--market-count": props.data.length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
props.onInspectingChange(false)
}}
>
<div data-slot="market-labels">
<For each={props.data}>
@@ -1028,11 +1028,8 @@ function MarketShare(props: {
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
onBlur={() => props.onInspectingChange(false)}
onClick={() => inspectDay(index())}
onFocus={() => inspectDay(index())}
onPointerEnter={() => inspectDay(index())}
onClick={() => props.onActiveIndexChange(index())}
onPointerEnter={() => props.onActiveIndexChange(index())}
>
<span data-slot="market-axis-label">
<span data-slot="market-total">{formatTrillions(day.total)}</span>
@@ -1052,11 +1049,8 @@ function MarketShare(props: {
type="button"
aria-label={`${day.date} ${formatTrillions(day.total)}`}
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
onBlur={() => props.onInspectingChange(false)}
onClick={() => inspectDay(index())}
onFocus={() => inspectDay(index())}
onPointerEnter={() => inspectDay(index())}
onClick={() => props.onActiveIndexChange(index())}
onPointerEnter={() => props.onActiveIndexChange(index())}
>
<For each={stackedMarketAuthors(day, props.authorOrder)}>
{(item) => (
@@ -1096,45 +1090,6 @@ function MarketShare(props: {
)}
</For>
</div>
<Show when={props.inspecting ? props.data[props.activeIndex] : undefined} keyed>
{(day) => (
<div
id="market-share-tooltip"
data-component="chart-tooltip"
data-placement={props.activeIndex > props.data.length * 0.62 ? "left" : "right"}
role="tooltip"
style={
{
"--market-tooltip-left": `${((props.activeIndex + 0.5) / props.data.length) * 100}%`,
"--market-tooltip-right": `${100 - ((props.activeIndex + 0.5) / props.data.length) * 100}%`,
} as JSX.CSSProperties
}
>
<strong>{day.date}</strong>
<span>
{formatTrillions(day.total)} {i18n.t("home.total")}
</span>
<div data-slot="tooltip-divider" />
<For each={rankedMarketAuthors(day)}>
{(item, index) => (
<p
data-active={props.activeAuthor === item.author ? "true" : undefined}
data-muted={
props.activeAuthor !== undefined && props.activeAuthor !== item.author ? "true" : undefined
}
>
<span data-slot="tooltip-label">
<i style={{ background: getRankColor(item.author, index(), props.authorOrder, marketColors) }} />
<span data-slot="tooltip-name">{item.author}</span>
</span>
<em>{formatTrillions(item.tokens)}</em>
<b>{item.share.toFixed(1)}%</b>
</p>
)}
</For>
</div>
)}
</Show>
</div>
)
}
@@ -1293,10 +1248,6 @@ function getMarketSegmentColor(author: string, color: string, activeAuthor: stri
return "var(--stats-bar-idle)"
}
function rankedMarketAuthors(day: MarketDay) {
return day.authors.toSorted((a, b) => b.tokens - a.tokens || a.author.localeCompare(b.author))
}
function stackedMarketAuthors(day: MarketDay, order: Map<string, number>) {
return day.authors
.map((author, index) => ({ author, index }))
@@ -1351,6 +1302,16 @@ function formatMarketDate(day: MarketDay | undefined, fallback: string) {
return formatMarketDateLabel(day.date)
}
function formatMarketRange(data: MarketDay[], fallback: string) {
const first = data[0]?.date
const last = data[data.length - 1]?.date
if (!first || !last) return fallback
const start = marketDateParts(first).start
const end = marketDateParts(last).end
if (start === end) return formatMarketDateLabel(start)
return `${start} ${new Date().getFullYear()}${end} ${new Date().getFullYear()}`
}
function formatMarketDateLabel(label: string) {
const parts = marketDateParts(label)
const year = new Date().getFullYear()
@@ -1 +0,0 @@
CREATE INDEX `idx_country_model_range` ON `geo_stat` (`model`,`provider`,`grain`,`dataset`,`client`,`source`,`tier`,`period_key`);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
@@ -104,16 +104,6 @@ export const geoStat = mysqlTable(
index("idx_country").on(table.country, table.grain, table.period_key),
index("idx_continent").on(table.continent, table.grain, table.period_key),
index("idx_country_model").on(table.model, table.country, table.grain, table.period_key),
index("idx_country_model_range").on(
table.model,
table.provider,
table.grain,
table.dataset,
table.client,
table.source,
table.tier,
table.period_key,
),
],
)
+99 -86
View File
@@ -1,7 +1,9 @@
import { Client } from "@planetscale/database"
import { Effect } from "effect"
import { Resource } from "sst/resource"
import type { ModelStatMetric } from "./model"
import { DatabaseError } from "../database"
import type { GeoStatMetric } from "./geo"
import { ModelStatRepo, type ModelStatMetric } from "./model"
import { statProvider } from "./model-normalization"
import { isMissingRetentionTable } from "./retention"
import { DATA_SITE_TIERS, normalizeTier } from "./stat"
@@ -75,7 +77,7 @@ export type StatsModelData = {
}
usage: ModelUsagePoint[]
tokenMix: ModelMixEntry[]
country: CountryEntry[]
country: Record<UsageRange, CountryEntry[]>
peers: ModelPeerEntry[]
}
export type StatsLabData = {
@@ -125,7 +127,7 @@ export type StatsHomeData = {
cacheRatio: Record<TokenProduct, CacheRatioEntry[]>
sessionCost: Record<TokenProduct, SessionCostEntry[]>
retention: RetentionEntry[]
country: CountryEntry[]
country: Record<UsageRange, CountryEntry[]>
}
export class StatsDataError extends Error {
@@ -144,8 +146,6 @@ const RETENTION_MODEL_LIMIT = 15
const RETENTION_MIN_ELIGIBLE_USER_WEEKS = 100
const RETENTION_COHORT_WEEKS = 7
const TOP_MODEL_SEGMENT_LIMIT = 9
const QUERY_CACHE_TTL_MS = 5 * 60 * 1000
const QUERY_CACHE_MAX_ENTRIES = 256
// Preserve the response shape while the public site presents Go and Free as one cohort.
const SITE_PRODUCT = "Go"
const SITE_TIER_PLACEHOLDERS = DATA_SITE_TIERS.map(() => "?").join(", ")
@@ -156,10 +156,8 @@ type StatMetricRow = Omit<ModelStatMetric, "updatedAt"> & {
periodStart: number
updatedAt: number
}
type CountryTotalRow = {
country: string
continent: string
tokens: number
type GeoMetricRow = Omit<GeoStatMetric, "updatedAt"> & {
periodStart: number
updatedAt: number
}
export type RetentionMetricRow = {
@@ -189,16 +187,15 @@ type ModelAggregate = {
}
type RawRow = Record<string, unknown>
type CachedQuery = { expiresAt: number; value: Promise<RawRow[]> }
const queryCache = new Map<string, CachedQuery>()
export function getStatsHomeData(): Effect.Effect<StatsHomeData, StatsDataError> {
return Effect.tryPromise({
try: async () => {
const [modelRows, retentionRows] = await Promise.all([listModelDaily(), listRetentionWeekly()])
const window = modelRowsWindow(modelRows, "2M")
const geoRows = window ? await listCountryTotals(window) : []
const [modelRows, geoRows, retentionRows] = await Promise.all([
listModelDaily(),
listGeoDaily(),
listRetentionWeekly(),
])
return buildStatsHomeData(modelRows, geoRows, retentionRows)
},
catch: (cause) => new StatsDataError(cause),
@@ -215,12 +212,13 @@ export function getStatsModelData(
const normalized = modelRows.flatMap(normalizeStatRow)
const resolvedModel = resolveModelName(model, normalized, provider)
if (!resolvedModel) return null
const window = modelRowsWindow(modelRows, "2M")
const resolvedProvider = resolveModelProvider(resolvedModel, normalized, provider)
return buildStatsModelData(
resolvedModel,
modelRows,
window ? await listCountryTotals(window, { model: resolvedModel, provider: resolvedProvider }) : [],
await listGeoDaily({
model: resolvedModel,
provider: resolveModelProvider(resolvedModel, normalized, provider),
}),
provider,
retentionRows,
)
@@ -241,8 +239,8 @@ async function listModelDaily(): Promise<ModelStatMetric[]> {
await queryRows(
`select period_key, updated_at, tier, provider, model, sessions, unique_users, input_tokens,
output_tokens, reasoning_tokens, cache_read_tokens, total_tokens, input_cost_microcents, output_cost_microcents,
total_cost_microcents from model_stat where grain = 'day' and dataset = 'zen' and client = 'all'
and source = 'all' and tier in (${SITE_TIER_PLACEHOLDERS}) order by period_key`,
total_cost_microcents from model_stat where grain = 'day' and client = 'all' and source = 'all'
and tier in (${SITE_TIER_PLACEHOLDERS}) order by period_key`,
DATA_SITE_TIERS,
)
).map((row) => ({
@@ -264,10 +262,7 @@ async function listModelDaily(): Promise<ModelStatMetric[]> {
}))
}
async function listCountryTotals(
window: DateWindow,
opts?: { provider?: string; model?: string },
): Promise<CountryTotalRow[]> {
async function listGeoDaily(opts?: { provider?: string; model?: string }): Promise<GeoStatMetric[]> {
const scope =
opts?.model && opts.provider
? "and provider = ? and model = ?"
@@ -277,16 +272,20 @@ async function listCountryTotals(
const params = opts?.model && opts.provider ? [opts.provider, opts.model] : opts?.model ? [opts.model] : []
return (
await queryRows(
`select country, max(continent) as continent, sum(total_tokens) as total_tokens, max(updated_at) as updated_at
from geo_stat where grain = 'day' and dataset = 'zen' and client = 'all' and source = 'all'
and tier in (${SITE_TIER_PLACEHOLDERS}) ${scope} and period_key >= ? and period_key < ? group by country`,
[...DATA_SITE_TIERS, ...params, periodKey(window.start), periodKey(window.end)],
`select period_key, updated_at, tier, provider, model, country, continent, total_tokens from geo_stat
where grain = 'day' and client = 'all' and source = 'all'
and tier in (${SITE_TIER_PLACEHOLDERS}) ${scope} order by period_key`,
[...DATA_SITE_TIERS, ...params],
)
).map((row) => ({
updatedAt: dateValue(row.updated_at).getTime(),
country: stringValue(row.country) || "ZZ",
periodKey: stringValue(row.period_key),
updatedAt: dateValue(row.updated_at),
tier: stringValue(row.tier),
provider: stringValue(row.provider),
model: stringValue(row.model),
country: stringValue(row.country),
continent: stringValue(row.continent),
tokens: numberValue(row.total_tokens),
totalTokens: numberValue(row.total_tokens),
}))
}
@@ -312,21 +311,7 @@ async function listRetentionWeekly(): Promise<RetentionMetricRow[]> {
}
async function queryRows(query: string, params: string[] = []) {
const key = JSON.stringify([query, params])
const now = Date.now()
const cached = queryCache.get(key)
if (cached && cached.expiresAt > now) return cached.value
if (cached) queryCache.delete(key)
const value = new Client({ url: databaseUrl() }).execute(query, params).then((result) => result.rows as RawRow[])
const entry = { expiresAt: now + QUERY_CACHE_TTL_MS, value }
queryCache.set(key, entry)
if (queryCache.size > QUERY_CACHE_MAX_ENTRIES) queryCache.delete(queryCache.keys().next().value!)
return value.catch((cause) => {
if (queryCache.get(key) === entry) queryCache.delete(key)
throw cause
})
return (await new Client({ url: databaseUrl() }).execute(query, params)).rows as RawRow[]
}
function databaseUrl() {
@@ -345,27 +330,31 @@ function dateValue(value: unknown) {
return value instanceof Date ? value : new Date(stringValue(value))
}
export function getStatsModelsComparisonData(
export const getStatsModelsComparisonData: (
models: readonly StatsModelComparisonInput[],
): Effect.Effect<StatsModelComparisonData, StatsDataError> {
return Effect.tryPromise({
try: async () => {
const [rows, retentionRows] = await Promise.all([listModelDaily(), listRetentionWeekly()])
const entries = models.map((model) =>
toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider, retentionRows)),
)
const latest = entries
.map((model) => model?.updatedAt)
.flatMap((value) => (value ? [dateTime(value)] : []))
.toSorted((a, b) => b - a)[0]
return {
updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
models: entries,
}
},
catch: (cause) => new StatsDataError(cause),
})
}
) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelsComparison.getData")(
function* (models) {
const modelStats = yield* ModelStatRepo
const [rows, retentionRows] = yield* Effect.all([
modelStats.listDaily(),
Effect.tryPromise({
try: listRetentionWeekly,
catch: (cause) => DatabaseError.make({ cause }),
}),
])
const entries = models.map((model) =>
toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider, retentionRows)),
)
const latest = entries
.map((model) => model?.updatedAt)
.flatMap((value) => (value ? [dateTime(value)] : []))
.toSorted((a, b) => b - a)[0]
return {
updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
models: entries,
}
},
)
export const getStatsModelComparisonData = (
firstProvider: string,
@@ -380,15 +369,17 @@ export const getStatsModelComparisonData = (
function buildStatsHomeData(
modelRows: ModelStatMetric[],
countryRows: CountryTotalRow[],
geoRows: GeoStatMetric[],
retentionRows: RetentionMetricRow[],
): StatsHomeData {
const normalized = modelRows.flatMap(normalizeStatRow)
if (normalized.length === 0) return emptyStatsHomeData()
const geo = geoRows.flatMap(normalizeGeoRow)
const periods = [...normalized, ...geo]
if (periods.length === 0) return emptyStatsHomeData()
const earliest = Math.min(...normalized.map((row) => row.periodStart))
const latest = Math.max(...normalized.map((row) => row.periodStart))
const latestUpdate = Math.max(...normalized.map((row) => row.updatedAt), ...countryRows.map((row) => row.updatedAt))
const earliest = Math.min(...periods.map((row) => row.periodStart))
const latest = Math.max(...periods.map((row) => row.periodStart))
const latestUpdate = Math.max(...periods.map((row) => row.updatedAt))
return {
updatedAt: new Date(latestUpdate).toISOString(),
@@ -416,7 +407,7 @@ function buildStatsHomeData(
),
),
leaderboard: createUsageProductRecord((product) =>
createRangeRecord((_range) => buildLeaderboard(normalized, product, getWindow("1W", earliest, latest))),
createRangeRecord((range) => buildLeaderboard(normalized, product, getWindow("1W", earliest, latest))),
),
market: createRangeRecord((range) => buildMarketShare(normalized, "Go", range, getWindow(range, earliest, latest))),
tokenCost: createTokenProductRecord((product) =>
@@ -431,18 +422,19 @@ function buildStatsHomeData(
retention: buildRetentionEntries(retentionRows)
.filter((item) => item.rank !== null)
.slice(0, RETENTION_MODEL_LIMIT),
country: buildCountryStats(countryRows),
country: createRangeRecord((range) => buildCountryStats(geo, getWindow(range, earliest, latest))),
}
}
function buildStatsModelData(
modelParam: string,
modelRows: ModelStatMetric[],
countryRows: CountryTotalRow[],
geoRows: GeoStatMetric[],
providerParam?: string,
retentionRows: RetentionMetricRow[] = [],
): StatsModelData | null {
const normalized = modelRows.flatMap(normalizeStatRow)
const geo = geoRows.flatMap(normalizeGeoRow)
if (normalized.length === 0) return null
const model = resolveModelName(modelParam, normalized, providerParam)
@@ -505,7 +497,7 @@ function buildStatsModelData(
},
usage: buildModelUsage(currentRows, window, "2M"),
tokenMix: buildModelTokenMix(current),
country: buildCountryStats(countryRows),
country: createRangeRecord((range) => buildCountryStats(geo, getWindow(range, earliest, latest))),
peers: buildModelPeers(rankPeers, peerRank, peerTokens),
}
}
@@ -587,7 +579,7 @@ function emptyStatsHomeData(): StatsHomeData {
cacheRatio: createTokenProductRecord(() => []),
sessionCost: createTokenProductRecord(() => []),
retention: [],
country: [],
country: createRangeRecord(() => []),
}
}
@@ -715,8 +707,8 @@ function buildMarketShare(rows: StatMetricRow[], product: UsageProduct, range: U
})
}
function buildCountryStats(rows: CountryTotalRow[]) {
const countries = rows
function buildCountryStats(rows: GeoMetricRow[], window: DateWindow) {
const countries = aggregateByCountry(rowsForProduct(rows, SITE_PRODUCT, window.start, window.end))
.filter((item) => item.tokens > 0 && item.country !== "AQ")
.toSorted((a, b) => b.tokens - a.tokens)
const totalTokens = countries.reduce((sum, item) => sum + item.tokens, 0)
@@ -870,6 +862,19 @@ function aggregateByProvider(rows: { provider: string; totalTokens: number }[])
)
}
function aggregateByCountry(rows: GeoMetricRow[]) {
return Object.values(
rows.reduce<Record<string, { country: string; continent: string; tokens: number }>>((result, row) => {
result[row.country] = {
country: row.country,
continent: result[row.country]?.continent || row.continent,
tokens: (result[row.country]?.tokens ?? 0) + row.totalTokens,
}
return result
}, {}),
)
}
function combineRowsForModel(model: string, rows: StatMetricRow[]): ModelAggregate {
const aggregate = rows.reduce<ModelAggregate | undefined>(
(result, row) => combineModelAggregate(result, row),
@@ -995,20 +1000,28 @@ function normalizeStatRow(row: ModelStatMetric): StatMetricRow[] {
]
}
function modelRowsWindow(rows: ModelStatMetric[], range: UsageRange): DateWindow | undefined {
const periods = rows.map((row) => periodKeyTime(row.periodKey)).filter(Number.isFinite)
if (periods.length === 0) return undefined
return getWindow(range, Math.min(...periods), Math.max(...periods))
function normalizeGeoRow(row: GeoStatMetric): GeoMetricRow[] {
const periodStart = periodKeyTime(row.periodKey)
const updatedAt = dateTime(row.updatedAt)
if (!Number.isFinite(periodStart) || !Number.isFinite(updatedAt)) return []
return [
{
...row,
periodStart,
updatedAt,
tier: normalizeTier(row.tier),
provider: row.provider === "all" ? "all" : statProvider(row.model, undefined, row.provider) || "unknown",
model: row.model || "all",
country: row.country || "ZZ",
continent: row.continent || "",
},
]
}
function dateTime(value: Date | string) {
return (value instanceof Date ? value : new Date(value)).getTime()
}
function periodKey(value: number) {
return new Date(value).toISOString().slice(0, 10)
}
function periodKeyTime(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
if (!match) return Number.NaN
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.18.27",
"version": "1.18.26",
"private": true,
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -465,7 +465,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
return
}
const title = session.title.length > 40 ? session.title.slice(0, 37) + "" : session.title
const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
renderer.setTerminalTitle(`OC | ${title}`)
return
}
@@ -1051,7 +1051,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
toast.show({
variant: "info",
message: `Updating to v${version}`,
message: `Updating to v${version}...`,
duration: 30000,
})
@@ -51,7 +51,7 @@ export function DialogConsoleOrg() {
if (listed === undefined) {
return [
{
title: "Loading orgs",
title: "Loading orgs...",
value: "loading",
onSelect: () => {},
},
@@ -113,7 +113,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
if (showError()) return []
const data = directoryData()
const current = currentRoot()?.directory
if (directories.loading && !data && !current) return [{ title: "Loading project directories", value: undefined }]
if (directories.loading && !data && !current) return [{ title: "Loading project directories...", value: undefined }]
const roots = [...(data ?? [])]
if (current && !roots.some((item) => item.directory === current)) roots.unshift({ directory: current })
roots.sort((a, b) => {
@@ -297,7 +297,7 @@ function AutoMethod(props: AutoMethodProps) {
<Link href={props.authorization.url} fg={theme.primary} />
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
</box>
<text fg={theme.textMuted}>Waiting for authorization</text>
<text fg={theme.textMuted}>Waiting for authorization...</text>
<text fg={theme.text}>
c <span style={{ fg: theme.textMuted }}>copy</span>
</text>
+1 -1
View File
@@ -51,7 +51,7 @@ export function DialogSkill(props: DialogSkillProps) {
return (
<DialogSelect
title="Skills"
placeholder="Search skills"
placeholder="Search skills..."
options={options()}
renderFilter={!showError()}
locked={showError()}
@@ -39,7 +39,7 @@ export function DialogWorkspaceList() {
return {
title:
removing() === workspace.id
? "Deleting"
? "Deleting..."
: deleting() === workspace.id
? `Delete ${workspace.name}? Press delete again`
: workspace.name,
@@ -219,7 +219,7 @@ function buildIssueURL(message: string, stack: string) {
// clipped trace is obvious. searchParams.set handles encoding without throwing,
// so measuring url.toString() is both correct and safe on any input.
const MAX_URL_LENGTH = 6000
const marker = "\n (truncated)"
const marker = "\n... (truncated)"
const head = `The opencode TUI crashed with an unexpected error.\n\n**Error:** ${message}\n\n**Stack trace:**\n`
const setBody = (body: string) => url.searchParams.set("description", head + "```\n" + body + "\n```")
+3 -3
View File
@@ -1313,10 +1313,10 @@ export function Prompt(props: PromptProps) {
if (store.mode === "shell") {
if (!shell().length) return undefined
const example = shell()[store.placeholder % shell().length]
return `Run a command "${example}"`
return `Run a command... "${example}"`
}
if (!list().length) return undefined
return `Ask anything "${list()[store.placeholder % list().length]}"`
return `Ask anything... "${list()[store.placeholder % list().length]}"`
})
const spinnerDef = createMemo(() => {
@@ -1537,7 +1537,7 @@ export function Prompt(props: PromptProps) {
if (!r) return
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
return "gemini is way too hot right now"
if (r.message.length > 80) return r.message.slice(0, 80) + ""
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
return r.message
})
const isTruncated = createMemo(() => {
@@ -5,7 +5,7 @@ import { Spinner } from "./spinner"
export function StartupLoading(props: { ready: () => boolean }) {
const theme = useTheme().theme
const [show, setShow] = createSignal(false)
const text = createMemo(() => (props.ready() ? "Finishing startup" : "Loading plugins"))
const text = createMemo(() => (props.ready() ? "Finishing startup..." : "Loading plugins..."))
let wait: NodeJS.Timeout | undefined
let hold: NodeJS.Timeout | undefined
let stamp = 0
@@ -766,7 +766,7 @@ function DiffViewer(props: { api: TuiPluginApi }) {
<Match when={diff.loading}>
<Separator axis="x" />
<box flexGrow={1} paddingLeft={1}>
<text fg={theme().textMuted}>Loading diff</text>
<text fg={theme().textMuted}>Loading diff...</text>
</box>
</Match>
<Match when={!diff.loading && files().length === 0}>
@@ -49,7 +49,7 @@ function Install(props: { api: TuiPluginApi }) {
title="Install plugin"
placeholder="npm package name"
busy={busy()}
busyText="Installing plugin"
busyText="Installing plugin..."
description={() => (
<box flexDirection="row" gap={1}>
<text fg={props.api.theme.current.textMuted}>scope:</text>
+26 -15
View File
@@ -1812,7 +1812,7 @@ function GenericTool(props: ToolProps) {
<Show
when={props.output && ctx.showGenericToolOutput()}
fallback={
<InlineTool icon="⚙" pending="Writing command" complete={true} part={props.part}>
<InlineTool icon="⚙" pending="Writing command..." complete={true} part={props.part}>
{props.tool} {input(props.input)}
</InlineTool>
}
@@ -2094,7 +2094,7 @@ function Shell(props: ToolProps) {
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="$" pending="Writing command" complete={stringValue(props.input.command)} part={props.part}>
<InlineTool icon="$" pending="Writing command..." complete={stringValue(props.input.command)} part={props.part}>
{stringValue(props.input.command)}
</InlineTool>
</Match>
@@ -2126,7 +2126,12 @@ function Write(props: ToolProps) {
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing write…" complete={stringValue(props.input.filePath)} part={props.part}>
<InlineTool
icon="←"
pending="Preparing write..."
complete={stringValue(props.input.filePath)}
part={props.part}
>
Write {pathFormatter.format(stringValue(props.input.filePath))}
</InlineTool>
</Match>
@@ -2137,7 +2142,7 @@ function Write(props: ToolProps) {
function Glob(props: ToolProps) {
const pathFormatter = usePathFormatter()
return (
<InlineTool icon="✱" pending="Finding files" complete={stringValue(props.input.pattern)} part={props.part}>
<InlineTool icon="✱" pending="Finding files..." complete={stringValue(props.input.pattern)} part={props.part}>
Glob "{stringValue(props.input.pattern)}"{" "}
<Show when={stringValue(props.input.path)}>in {pathFormatter.format(stringValue(props.input.path))} </Show>
<Show when={numberValue(props.metadata.count)}>
@@ -2162,7 +2167,7 @@ function Read(props: ToolProps) {
<>
<InlineTool
icon="→"
pending="Reading file"
pending="Reading file..."
complete={stringValue(props.input.filePath)}
spinner={isRunning()}
part={props.part}
@@ -2185,7 +2190,7 @@ function Read(props: ToolProps) {
function Grep(props: ToolProps) {
const pathFormatter = usePathFormatter()
return (
<InlineTool icon="✱" pending="Searching content" complete={stringValue(props.input.pattern)} part={props.part}>
<InlineTool icon="✱" pending="Searching content..." complete={stringValue(props.input.pattern)} part={props.part}>
Grep "{stringValue(props.input.pattern)}"{" "}
<Show when={stringValue(props.input.path)}>in {pathFormatter.format(stringValue(props.input.path))} </Show>
<Show when={numberValue(props.metadata.matches)}>
@@ -2197,7 +2202,7 @@ function Grep(props: ToolProps) {
function WebFetch(props: ToolProps) {
return (
<InlineTool icon="%" pending="Fetching from the web" complete={stringValue(props.input.url)} part={props.part}>
<InlineTool icon="%" pending="Fetching from the web..." complete={stringValue(props.input.url)} part={props.part}>
WebFetch {stringValue(props.input.url)}
</InlineTool>
)
@@ -2205,7 +2210,7 @@ function WebFetch(props: ToolProps) {
function WebSearch(props: ToolProps) {
return (
<InlineTool icon="◈" pending="Searching web" complete={stringValue(props.input.query)} part={props.part}>
<InlineTool icon="◈" pending="Searching web..." complete={stringValue(props.input.query)} part={props.part}>
{webSearchProviderLabel(props.metadata.provider)} "{stringValue(props.input.query)}"{" "}
<Show when={numberValue(props.metadata.numResults)}>({numberValue(props.metadata.numResults)} results)</Show>
</InlineTool>
@@ -2295,7 +2300,7 @@ function Task(props: ToolProps) {
color={retry() ? theme.error : undefined}
spinner={isRunning()}
complete={stringValue(props.input.description)}
pending="Delegating"
pending="Delegating..."
part={props.part}
onClick={() => {
if (sessionID()) {
@@ -2432,7 +2437,7 @@ function Edit(props: ToolProps) {
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="←" pending="Preparing edit" complete={stringValue(props.input.filePath)} part={props.part}>
<InlineTool icon="←" pending="Preparing edit..." complete={stringValue(props.input.filePath)} part={props.part}>
Edit {pathFormatter.format(stringValue(props.input.filePath))} {input({ replaceAll: props.input.replaceAll })}
</InlineTool>
</Match>
@@ -2508,7 +2513,7 @@ function ApplyPatch(props: ToolProps) {
</For>
</Match>
<Match when={true}>
<InlineTool icon="%" pending="Preparing patch" failure="Patch failed" complete={false} part={props.part}>
<InlineTool icon="%" pending="Preparing patch..." failure="Patch failed" complete={false} part={props.part}>
Patch
</InlineTool>
</Match>
@@ -2528,8 +2533,14 @@ function TodoWrite(props: ToolProps) {
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="⚙" pending="Updating todos…" failure="Todo update failed" complete={false} part={props.part}>
Updating todos
<InlineTool
icon="⚙"
pending="Updating todos..."
failure="Todo update failed"
complete={false}
part={props.part}
>
Updating todos...
</InlineTool>
</Match>
</Switch>
@@ -2564,7 +2575,7 @@ function Question(props: ToolProps) {
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="→" pending="Asking questions" complete={count()} part={props.part}>
<InlineTool icon="→" pending="Asking questions..." complete={count()} part={props.part}>
Asked {count()} question{count() !== 1 ? "s" : ""}
</InlineTool>
</Match>
@@ -2574,7 +2585,7 @@ function Question(props: ToolProps) {
function Skill(props: ToolProps) {
return (
<InlineTool icon="→" pending="Loading skill" complete={stringValue(props.input.name)} part={props.part}>
<InlineTool icon="→" pending="Loading skill..." complete={stringValue(props.input.name)} part={props.part}>
Skill "{stringValue(props.input.name)}"
</InlineTool>
)
+2 -2
View File
@@ -99,11 +99,11 @@ export function DialogPrompt(props: DialogPromptProps) {
cursorStyle={tuiConfig.cursor}
/>
<Show when={props.busy}>
<Spinner color={theme.textMuted}>{props.busyText ?? "Working"}</Spinner>
<Spinner color={theme.textMuted}>{props.busyText ?? "Working..."}</Spinner>
</Show>
</box>
<box paddingBottom={1} gap={1} flexDirection="row">
<Show when={!props.busy} fallback={<text fg={theme.textMuted}>processing</text>}>
<Show when={!props.busy} fallback={<text fg={theme.textMuted}>processing...</text>}>
<Show when={submitShortcut()}>
<text fg={theme.text}>
{submitShortcut()} <span style={{ fg: theme.textMuted }}>submit</span>
@@ -77,7 +77,7 @@ describe("DiffViewerFileTree", () => {
<DiffViewerFileTree width={32} files={[]} loading={false} error={undefined} theme={theme} />
))
expect(loading).not.toContain("Loading diff")
expect(loading).not.toContain("Loading diff...")
expect(loading).not.toContain("No files")
expect(failed).not.toContain("Failed to load diff")
expect(failed).not.toContain("No files")
@@ -195,7 +195,7 @@ function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: Scrol
function FailedPendingToolFixture() {
return (
<InlineToolRow icon="%" complete={false} pending="Preparing patch" failed={true} failure="Patch failed">
<InlineToolRow icon="%" complete={false} pending="Preparing patch..." failed={true} failure="Patch failed">
Patch
</InlineToolRow>
)
@@ -203,7 +203,7 @@ function FailedPendingToolFixture() {
function FailedCompleteToolFixture() {
return (
<InlineToolRow icon="→" complete={true} pending="Reading file" failed={true} failure="Read failed">
<InlineToolRow icon="→" complete={true} pending="Reading file..." failed={true} failure="Read failed">
Read src/index.ts
</InlineToolRow>
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.27",
"version": "1.18.26",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.27",
"version": "1.18.26",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
-12
View File
@@ -80,18 +80,6 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
---
## أين يمكنني استخدامه؟
صُمّم OpenCode Go للاستخدام مع [OpenCode](https://opencode.ai) وغيره من وكلاء البرمجة الشائعين الذين ينشئون أنواعًا مماثلة من الطلبات.
تتم مراقبة حركة المرور لرصد الاستخدام المسيء الذي يؤدي إلى تدهور تجربة المستخدمين الآخرين.
لتجنب وضع علامة على حسابك، تأكد من أن الأداة التي تستخدمها
1\. لا تنشئ حركة مرور مسيئة
2\. تعرّف عن نفسها بشكل صحيح (من دون معرّفات وكيل مستخدم عامة)
3\. تتضمن ترويسة `x-opencode-session` حتى نتمكن من تحسين التخزين المؤقت للمطالبات
## حدود الاستخدام
يتضمن OpenCode Go الحدود التالية:
-12
View File
@@ -90,18 +90,6 @@ Lista modela se može mijenjati dok testiramo i dodajemo nove.
---
## Gdje ga mogu koristiti?
OpenCode Go je osmišljen za korištenje s [OpenCode-om](https://opencode.ai) i drugim popularnim agentima za programiranje koji generišu slične vrste zahtjeva.
Saobraćaj se nadzire radi otkrivanja zloupotrebe koja narušava iskustvo drugih korisnika.
Kako vaš račun ne bi bio označen, pobrinite se da alat koji koristite
1\. ne generiše saobraćaj koji predstavlja zloupotrebu
2\. se ispravno identifikuje (bez generičkih User-Agent identifikatora)
3\. uključuje zaglavlje `x-opencode-session` kako bismo mogli optimizovati keširanje promptova
## Ograničenja upotrebe
OpenCode Go uključuje sljedeća ograničenja:
+2 -3
View File
@@ -374,7 +374,7 @@ You can configure the providers and models you want to use in your OpenCode conf
The `small_model` option configures a separate model for lightweight tasks like title generation. By default, OpenCode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model.
Provider options can include `timeout`, `headerTimeout`, `chunkTimeout`, and `setCacheKey`:
Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`:
```json title="opencode.json"
{
@@ -392,8 +392,7 @@ Provider options can include `timeout`, `headerTimeout`, `chunkTimeout`, and `se
```
- `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable.
- `headerTimeout` - Timeout in milliseconds to wait for response headers (default: 300000, or 5 minutes). This timer stops once headers arrive and does not limit the streamed response body. Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks (default: 300000, or 5 minutes). If no chunk arrives in time, the request is aborted. Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks. If no chunk arrives in time, the request is aborted.
- `setCacheKey` - Ensure a cache key is always set for designated provider.
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).
-12
View File
@@ -90,18 +90,6 @@ Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye
---
## Hvor kan jeg bruge det?
OpenCode Go er designet til brug med [OpenCode](https://opencode.ai) og andre populære kodningsagenter, der genererer lignende typer anmodninger.
Trafikken overvåges for misbrug, der forringer oplevelsen for andre brugere.
For at sikre, at din konto ikke bliver markeret, skal du sørge for, at det værktøj, du bruger,
1\. ikke genererer misbrugstrafik
2\. identificerer sig korrekt (ingen generiske User-Agent-identifikatorer)
3\. inkluderer `x-opencode-session`-headeren, så vi kan optimere prompt-caching
## Forbrugsgrænser
OpenCode Go inkluderer følgende grænser:
-12
View File
@@ -82,18 +82,6 @@ Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufüge
---
## Wo kann ich es verwenden?
OpenCode Go wurde für die Verwendung mit [OpenCode](https://opencode.ai) und anderen beliebten Coding-Agenten entwickelt, die ähnliche Arten von Anfragen erzeugen.
Der Datenverkehr wird auf missbräuchlichen Traffic überwacht, der das Nutzungserlebnis anderer beeinträchtigt.
Damit dein Konto nicht markiert wird, stelle sicher, dass das von dir verwendete Tool
1\. keinen missbräuchlichen Traffic erzeugt
2\. sich ordnungsgemäß identifiziert (keine allgemeinen User-Agents)
3\. den Header `x-opencode-session` enthält, damit wir das Prompt-Caching optimieren können
## Nutzungslimits
OpenCode Go beinhaltet die folgenden Limits:
-12
View File
@@ -90,18 +90,6 @@ La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos
---
## ¿Dónde puedo usarlo?
OpenCode Go está diseñado para usarse con [OpenCode](https://opencode.ai) y otros agentes de programación populares que generan tipos de peticiones similares.
El tráfico se supervisa para detectar tráfico abusivo que perjudique la experiencia de otros usuarios.
Para evitar que tu cuenta sea marcada, asegúrate de que la herramienta que usas
1\. no genere tráfico abusivo
2\. se identifique correctamente (sin agentes de usuario genéricos)
3\. incluya el encabezado `x-opencode-session` para que podamos optimizar el almacenamiento en caché de prompts
## Límites de uso
OpenCode Go incluye los siguientes límites:
-12
View File
@@ -80,18 +80,6 @@ La liste des modèles peut changer au fur et à mesure que nous en testons et en
---
## Où puis-je l'utiliser ?
OpenCode Go est conçu pour être utilisé avec [OpenCode](https://opencode.ai) et d'autres agents de codage populaires qui génèrent des types de requêtes similaires.
Le trafic est surveillé afin de détecter tout trafic abusif qui dégrade l'expérience des autres utilisateurs.
Pour éviter que votre compte ne soit signalé, assurez-vous que l'outil que vous utilisez
1\. ne génère pas de trafic abusif
2\. s'identifie correctement (pas d'agents utilisateur génériques)
3\. inclut l'en-tête `x-opencode-session` afin que nous puissions optimiser la mise en cache des prompts
## Limites d'utilisation
OpenCode Go inclut les limites suivantes :
+1 -2
View File
@@ -100,8 +100,7 @@ Traffic is monitored for abusive traffic that degrades the experience for other
To ensure your account does not get flagged, make sure the tool you're using
1\. does not generate abusive traffic
2\. properly identifies itself (no broad user agents)<br />
3\. includes the `x-opencode-session` header so we can optimize prompt caching
2\. properly identifies itself (no broad user agents)
## Usage limits
-12
View File
@@ -88,18 +88,6 @@ L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di
---
## Dove posso usarlo?
OpenCode Go è progettato per essere utilizzato con [OpenCode](https://opencode.ai) e altri agenti di programmazione popolari che generano tipi di richieste simili.
Il traffico viene monitorato per rilevare traffico abusivo che compromette l'esperienza degli altri utenti.
Per evitare che il tuo account venga segnalato, assicurati che lo strumento che utilizzi
1\. non generi traffico abusivo
2\. si identifichi correttamente (senza user agent generici)
3\. includa l'header `x-opencode-session` in modo da consentirci di ottimizzare il caching dei prompt
## Limiti di utilizzo
OpenCode Go include i seguenti limiti:
-12
View File
@@ -80,18 +80,6 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー
---
## どこで使用できますか?
OpenCode Goは、[OpenCode](https://opencode.ai)や、同様の種類のリクエストを生成するその他の一般的なコーディングエージェントで使用することを想定しています。
他のユーザーの利用体験を損なう不正なトラフィックがないか監視されています。
アカウントにフラグが付けられないよう、使用するツールが以下の条件を満たしていることを確認してください。
1\. 不正なトラフィックを生成しない
2\. 自身を適切に識別する(汎用的すぎるユーザーエージェントを使用しない)
3\. プロンプトキャッシュを最適化できるよう、`x-opencode-session`ヘッダーを含める
## 利用制限
OpenCode Goには以下の制限が含まれています:
-12
View File
@@ -80,18 +80,6 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다.
---
## 어디에서 사용할 수 있나요?
OpenCode Go는 [OpenCode](https://opencode.ai) 및 유사한 유형의 요청을 생성하는 다른 인기 코딩 에이전트와 함께 사용하도록 설계되었습니다.
다른 사용자의 이용 경험을 저해하는 악성 트래픽이 있는지 모니터링합니다.
계정에 플래그가 지정되지 않도록 사용 중인 도구가 다음 조건을 충족하는지 확인하세요.
1\. 악성 트래픽을 생성하지 않음
2\. 자체 정보를 올바르게 표시함(포괄적인 사용자 에이전트를 사용하지 않음)
3\. 프롬프트 캐싱을 최적화할 수 있도록 `x-opencode-session` 헤더를 포함함
## 사용 한도
OpenCode Go에는 다음과 같은 한도가 포함됩니다.
-12
View File
@@ -90,18 +90,6 @@ Listen over modeller kan endres etter hvert som vi tester og legger til nye.
---
## Hvor kan jeg bruke det?
OpenCode Go er utviklet for bruk med [OpenCode](https://opencode.ai) og andre populære kodeagenter som genererer lignende typer forespørsler.
Trafikken overvåkes for misbruk som forringer opplevelsen for andre brukere.
For å sikre at kontoen din ikke blir flagget, må du sørge for at verktøyet du bruker,
1\. ikke genererer misbrukstrafikk
2\. identifiserer seg korrekt (ingen generiske User-Agent-identifikatorer)
3\. inkluderer `x-opencode-session`-headeren, slik at vi kan optimalisere promptbufring
## Bruksgrenser
OpenCode Go inkluderer følgende grenser:
-12
View File
@@ -84,18 +84,6 @@ Lista modeli może ulec zmianie w miarę testowania i dodawania nowych.
---
## Gdzie można z tego korzystać?
OpenCode Go jest przeznaczony do użytku z [OpenCode](https://opencode.ai) i innymi popularnymi agentami kodującymi, którzy generują podobne rodzaje żądań.
Ruch jest monitorowany pod kątem nadużyć, które pogarszają komfort korzystania z usługi przez innych użytkowników.
Aby Twoje konto nie zostało oznaczone, upewnij się, że używane przez Ciebie narzędzie
1\. nie generuje ruchu stanowiącego nadużycie
2\. prawidłowo się identyfikuje (bez ogólnych identyfikatorów User-Agent)
3\. zawiera nagłówek `x-opencode-session`, abyśmy mogli zoptymalizować buforowanie promptów
## Limity użycia
OpenCode Go zawiera następujące limity:
@@ -874,54 +874,6 @@ Selecting a router model is a drop-in replacement for any other model — OpenCo
---
### Eden AI
[Eden AI](https://www.edenai.co/) is an EU-based gateway that serves models from many vendors over a single OpenAI-compatible API, with a separate EU endpoint for teams that need inference to stay in the EU.
1. Head over to the [Eden AI platform](https://app.edenai.run/user/register) to create an account and generate an API key.
2. Run the `/connect` command and search for **Eden AI**.
```txt
/connect
```
3. Enter your Eden AI API key.
```txt
┌ API key
└ enter
```
4. Run the `/models` command to select a model like _Mistral Large 3_ or _Claude Sonnet 5_.
```txt
/models
```
Eden AI model ids are themselves in `vendor/model` form, so a full reference has three segments, for example `edenai/anthropic/claude-sonnet-5`.
5. To keep requests on Eden AI's EU gateway, set its base URL.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"edenai": {
"options": {
"baseURL": "https://api.eu.edenai.run/v3"
}
}
}
}
```
The default is `https://api.edenai.run/v3`, so this swaps the global endpoint for the EU one. The EU endpoint serves the subset of the catalog that is available in the EU, so a model chosen in step 4 may not be reachable through it.
---
### FrogBot
1. Head over to the [FrogBot dashboard](https://app.frogbot.ai/signup), create an account, and generate an API key.
@@ -90,18 +90,6 @@ A lista de modelos pode mudar conforme testamos e adicionamos novos.
---
## Onde posso usá-lo?
O OpenCode Go foi projetado para ser usado com o [OpenCode](https://opencode.ai) e outros agentes de programação populares que geram tipos de requisições semelhantes.
O tráfego é monitorado para detectar tráfego abusivo que prejudique a experiência de outros usuários.
Para evitar que sua conta seja sinalizada, verifique se a ferramenta que você está usando
1\. não gera tráfego abusivo
2\. se identifica corretamente (sem user agents genéricos)
3\. inclui o header `x-opencode-session` para que possamos otimizar o cache de prompts
## Limites de uso
O OpenCode Go inclui os seguintes limites:
-12
View File
@@ -90,18 +90,6 @@ OpenCode Go работает так же, как и любой другой пр
---
## Где можно использовать OpenCode Go?
OpenCode Go предназначен для использования с [OpenCode](https://opencode.ai) и другими популярными агентами для программирования, которые создают запросы схожих типов.
Трафик отслеживается для выявления злоупотреблений, ухудшающих работу сервиса для других пользователей.
Чтобы ваша учетная запись не была отмечена, убедитесь, что используемый вами инструмент
1\. не создает трафик, представляющий собой злоупотребление
2\. правильно идентифицирует себя (без универсальных значений User-Agent)
3\. включает заголовок `x-opencode-session`, чтобы мы могли оптимизировать кеширование промптов
## Лимиты использования
OpenCode Go включает следующие лимиты:
-12
View File
@@ -80,18 +80,6 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร
---
## ใช้งานได้ที่ไหน?
OpenCode Go ออกแบบมาเพื่อใช้กับ [OpenCode](https://opencode.ai) และเอเจนต์เขียนโค้ดยอดนิยมอื่นๆ ที่สร้างคำขอในลักษณะเดียวกัน
ระบบจะตรวจสอบการรับส่งข้อมูลเพื่อค้นหาการใช้งานในทางที่ผิดซึ่งส่งผลกระทบต่อประสบการณ์ของผู้ใช้รายอื่น
เพื่อให้แน่ใจว่าบัญชีของคุณจะไม่ถูกตั้งค่าสถานะ โปรดตรวจสอบว่าเครื่องมือที่คุณใช้
1\. ไม่สร้างการรับส่งข้อมูลที่เป็นการใช้งานในทางที่ผิด
2\. ระบุตัวตนอย่างถูกต้อง (ไม่ใช้ข้อมูลระบุตัวแทนผู้ใช้แบบกว้างเกินไป)
3\. มีส่วนหัว `x-opencode-session` เพื่อให้เราสามารถปรับการแคชพรอมต์ให้เหมาะสมได้
## Usage limits
OpenCode Go มีขีดจำกัดดังต่อไปนี้:
-12
View File
@@ -80,18 +80,6 @@ Test edip yenilerini ekledikçe model listesi değişebilir.
---
## Nerede kullanabilirim?
OpenCode Go, [OpenCode](https://opencode.ai) ve benzer türde istekler üreten diğer popüler kodlama aracılarıyla kullanılmak üzere tasarlanmıştır.
Trafik, diğer kullanıcıların deneyimini olumsuz etkileyen kötüye kullanım amaçlı trafiğe karşı izlenir.
Hesabınızın işaretlenmemesi için kullandığınız aracın
1\. kötüye kullanım amaçlı trafik oluşturmadığından
2\. kendisini doğru şekilde tanıttığından (genel kapsamlı kullanıcı aracıları kullanmadığından)
3\. istem önbelleğe almayı optimize edebilmemiz için `x-opencode-session` başlığını içerdiğinden emin olun
## Kullanım limitleri
OpenCode Go aşağıdaki limitleri içerir:
@@ -80,18 +80,6 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。
---
## 可以在哪里使用?
OpenCode Go 适用于 [OpenCode](https://opencode.ai) 以及其他会产生类似请求的主流编程 Agent。
我们会监控流量,以识别影响其他用户体验的滥用行为。
为避免你的账户被标记为异常,请确保你使用的工具:
1\. 不产生滥用流量
2\. 明确标识自身(不要使用过于笼统的 user agent 标识)
3\. 包含 `x-opencode-session` 请求头,以便我们优化提示词缓存
## 使用限制
OpenCode Go 包含以下限制:
@@ -80,18 +80,6 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。
---
## 可以在哪裡使用?
OpenCode Go 適用於 [OpenCode](https://opencode.ai) 以及其他會產生類似請求的主流程式設計 Agent。
我們會監控流量,以識別影響其他使用者體驗的濫用行為。
為避免您的帳戶被標記為異常,請確保您使用的工具:
1\. 不產生濫用流量
2\. 明確標識自身(不要使用過於籠統的 user agent 識別資訊)
3\. 包含 `x-opencode-session` 請求標頭,以便我們最佳化提示詞快取
## 使用限制
OpenCode Go 包含以下限制:
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.18.27",
"version": "1.18.26",
"publisher": "sst-dev",
"repository": {
"type": "git",