diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index e7966cb48c7e..6c632f7e0729 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -1,10 +1,5 @@ name: "Setup Bun" description: "Setup Bun with caching and install dependencies" -inputs: - cross-compile: - description: "Pre-cache canary cross-compile binaries for all targets" - required: false - default: "false" runs: using: "composite" steps: @@ -21,12 +16,13 @@ runs: shell: bash run: | if [ "$RUNNER_ARCH" = "X64" ]; then + V=$(node -p "require('./package.json').packageManager.split('@')[1]") case "$RUNNER_OS" in macOS) OS=darwin ;; Linux) OS=linux ;; Windows) OS=windows ;; esac - echo "url=https://github.com/oven-sh/bun/releases/download/canary/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" + echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${V}/bun-${OS}-x64-baseline.zip" >> "$GITHUB_OUTPUT" fi - name: Setup Bun @@ -35,54 +31,6 @@ runs: bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} bun-download-url: ${{ steps.bun-url.outputs.url }} - - name: Pre-cache canary cross-compile binaries - if: inputs.cross-compile == 'true' - shell: bash - run: | - BUN_VERSION=$(bun --revision) - if echo "$BUN_VERSION" | grep -q "canary"; then - SEMVER=$(echo "$BUN_VERSION" | sed 's/^\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/') - echo "Bun version: $BUN_VERSION (semver: $SEMVER)" - CACHE_DIR="$HOME/.bun/install/cache" - mkdir -p "$CACHE_DIR" - TMP_DIR=$(mktemp -d) - for TARGET in linux-aarch64 linux-x64 linux-x64-baseline linux-aarch64-musl linux-x64-musl linux-x64-musl-baseline darwin-aarch64 darwin-x64 windows-x64 windows-x64-baseline; do - DEST="$CACHE_DIR/bun-${TARGET}-v${SEMVER}" - if [ -f "$DEST" ]; then - echo "Already cached: $DEST" - continue - fi - URL="https://github.com/oven-sh/bun/releases/download/canary/bun-${TARGET}.zip" - echo "Downloading $TARGET from $URL" - if curl -sfL -o "$TMP_DIR/bun.zip" "$URL"; then - unzip -qo "$TMP_DIR/bun.zip" -d "$TMP_DIR" - if echo "$TARGET" | grep -q "windows"; then - BIN_NAME="bun.exe" - else - BIN_NAME="bun" - fi - mv "$TMP_DIR/bun-${TARGET}/$BIN_NAME" "$DEST" - chmod +x "$DEST" - rm -rf "$TMP_DIR/bun-${TARGET}" "$TMP_DIR/bun.zip" - echo "Cached: $DEST" - # baseline bun resolves "bun-darwin-x64" to the baseline cache key - # so copy the modern binary there too - if [ "$TARGET" = "darwin-x64" ]; then - BASELINE_DEST="$CACHE_DIR/bun-darwin-x64-baseline-v${SEMVER}" - if [ ! -f "$BASELINE_DEST" ]; then - cp "$DEST" "$BASELINE_DEST" - echo "Cached (baseline alias): $BASELINE_DEST" - fi - fi - else - echo "Skipped: $TARGET (not available)" - fi - done - rm -rf "$TMP_DIR" - else - echo "Not a canary build ($BUN_VERSION), skipping pre-cache" - fi - - name: Install dependencies run: bun install shell: bash diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 20d2bc18d825..a7106667b116 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -27,7 +27,11 @@ jobs: opencode-app-id: ${{ vars.OPENCODE_APP_ID }} opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Install OpenCode + run: bun i -g opencode-ai + - name: Sync beta branch env: GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: bun script/beta.ts diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml index 1aafc5d1e3b1..f62afae4b9f1 100644 --- a/.github/workflows/docs-locale-sync.yml +++ b/.github/workflows/docs-locale-sync.yml @@ -65,9 +65,9 @@ jobs: "packages/web/src/content/docs/*/*.mdx": "allow", ".opencode": "allow", ".opencode/agent": "allow", - ".opencode/agent/glossary": "allow", + ".opencode/glossary": "allow", ".opencode/agent/translator.md": "allow", - ".opencode/agent/glossary/*.md": "allow" + ".opencode/glossary/*.md": "allow" }, "edit": { "*": "deny", @@ -76,7 +76,7 @@ jobs: "glob": { "*": "deny", "packages/web/src/content/docs*": "allow", - ".opencode/agent/glossary*": "allow" + ".opencode/glossary*": "allow" }, "task": { "*": "deny", @@ -90,7 +90,7 @@ jobs: "read": { "*": "deny", ".opencode/agent/translator.md": "allow", - ".opencode/agent/glossary/*.md": "allow" + ".opencode/glossary/*.md": "allow" } } } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cca7df5c4ed7..8d4c9038a7e4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -77,8 +77,6 @@ jobs: fetch-tags: true - uses: ./.github/actions/setup-bun - with: - cross-compile: "true" - name: Setup git committer id: committer @@ -90,7 +88,7 @@ jobs: - name: Build id: build run: | - ./packages/opencode/script/build.ts --all + ./packages/opencode/script/build.ts env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_RELEASE: ${{ needs.version.outputs.release }} diff --git a/.github/workflows/sign-cli.yml b/.github/workflows/sign-cli.yml index 89176223176d..d9d61fd800eb 100644 --- a/.github/workflows/sign-cli.yml +++ b/.github/workflows/sign-cli.yml @@ -20,12 +20,10 @@ jobs: fetch-tags: true - uses: ./.github/actions/setup-bun - with: - cross-compile: "true" - name: Build run: | - ./packages/opencode/script/build.ts --all + ./packages/opencode/script/build.ts - name: Upload unsigned Windows CLI id: upload_unsigned_windows_cli diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml index 94569f47312a..4c2aa960b2a8 100644 --- a/.github/workflows/vouch-check-issue.yml +++ b/.github/workflows/vouch-check-issue.yml @@ -42,15 +42,17 @@ jobs: throw error; } - // Parse the .td file for denounced users + // Parse the .td file for vouched and denounced users + const vouched = new Set(); 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 rest = trimmed.slice(1).trim(); + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; 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(); @@ -65,32 +67,50 @@ jobs: const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); if (!username) continue; - denounced.set(username.toLowerCase(), reason); + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); } // Check if the author is denounced const reason = denounced.get(author.toLowerCase()); - if (reason === undefined) { - core.info(`User ${author} is not denounced. Allowing issue.`); + 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}`); return; } - // 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, - }); + // Author is positively vouched — add label + if (!vouched.has(author.toLowerCase())) { + core.info(`User ${author} is not denounced or vouched. Allowing issue.`); + return; + } - await github.rest.issues.update({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, - state: 'closed', - state_reason: 'not_planned', + labels: ['Vouched'], }); - core.info(`Closed issue #${issueNumber} from denounced user ${author}`); + core.info(`Added vouched label to issue #${issueNumber} from ${author}`); diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml index 470b8e0a5ad7..51816dfb7590 100644 --- a/.github/workflows/vouch-check-pr.yml +++ b/.github/workflows/vouch-check-pr.yml @@ -6,6 +6,7 @@ on: permissions: contents: read + issues: write pull-requests: write jobs: @@ -42,15 +43,17 @@ jobs: throw error; } - // Parse the .td file for denounced users + // Parse the .td file for vouched and denounced users + const vouched = new Set(); 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 rest = trimmed.slice(1).trim(); + const isDenounced = trimmed.startsWith('-'); + const rest = isDenounced ? trimmed.slice(1).trim() : trimmed; 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(); @@ -65,29 +68,47 @@ jobs: const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1); if (!username) continue; - denounced.set(username.toLowerCase(), reason); + if (isDenounced) { + denounced.set(username.toLowerCase(), reason); + continue; + } + + vouched.add(username.toLowerCase()); } // Check if the author is denounced const reason = denounced.get(author.toLowerCase()); - if (reason === undefined) { - core.info(`User ${author} is not denounced. Allowing PR.`); + 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}`); return; } - // 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.', - }); + // 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.pulls.update({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, - state: 'closed', + issue_number: prNumber, + labels: ['Vouched'], }); - core.info(`Closed PR #${prNumber} from denounced user ${author}`); + core.info(`Added vouched label to PR #${prNumber} from ${author}`); diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index cf0524c21a8e..9604bf87f375 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -33,5 +33,6 @@ jobs: with: issue-id: ${{ github.event.issue.number }} comment-id: ${{ github.event.comment.id }} + roles: admin,maintain env: GITHUB_TOKEN: ${{ steps.committer.outputs.token }} diff --git a/.opencode/agent/translator.md b/.opencode/agent/translator.md index f0b3f8e9270b..6ef6d0847a37 100644 --- a/.opencode/agent/translator.md +++ b/.opencode/agent/translator.md @@ -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/agent/glossary/.md` when available (for example, `zh-cn.md`). +- Also apply locale-specific guidance from `.opencode/glossary/.md` when available (for example, `zh-cn.md`). - Do not modify fenced code blocks. - Output ONLY the translation (no commentary). diff --git a/.opencode/agent/glossary/README.md b/.opencode/glossary/README.md similarity index 100% rename from .opencode/agent/glossary/README.md rename to .opencode/glossary/README.md diff --git a/.opencode/agent/glossary/ar.md b/.opencode/glossary/ar.md similarity index 100% rename from .opencode/agent/glossary/ar.md rename to .opencode/glossary/ar.md diff --git a/.opencode/agent/glossary/br.md b/.opencode/glossary/br.md similarity index 100% rename from .opencode/agent/glossary/br.md rename to .opencode/glossary/br.md diff --git a/.opencode/agent/glossary/bs.md b/.opencode/glossary/bs.md similarity index 100% rename from .opencode/agent/glossary/bs.md rename to .opencode/glossary/bs.md diff --git a/.opencode/agent/glossary/da.md b/.opencode/glossary/da.md similarity index 100% rename from .opencode/agent/glossary/da.md rename to .opencode/glossary/da.md diff --git a/.opencode/agent/glossary/de.md b/.opencode/glossary/de.md similarity index 100% rename from .opencode/agent/glossary/de.md rename to .opencode/glossary/de.md diff --git a/.opencode/agent/glossary/es.md b/.opencode/glossary/es.md similarity index 100% rename from .opencode/agent/glossary/es.md rename to .opencode/glossary/es.md diff --git a/.opencode/agent/glossary/fr.md b/.opencode/glossary/fr.md similarity index 100% rename from .opencode/agent/glossary/fr.md rename to .opencode/glossary/fr.md diff --git a/.opencode/agent/glossary/ja.md b/.opencode/glossary/ja.md similarity index 100% rename from .opencode/agent/glossary/ja.md rename to .opencode/glossary/ja.md diff --git a/.opencode/agent/glossary/ko.md b/.opencode/glossary/ko.md similarity index 100% rename from .opencode/agent/glossary/ko.md rename to .opencode/glossary/ko.md diff --git a/.opencode/agent/glossary/no.md b/.opencode/glossary/no.md similarity index 100% rename from .opencode/agent/glossary/no.md rename to .opencode/glossary/no.md diff --git a/.opencode/agent/glossary/pl.md b/.opencode/glossary/pl.md similarity index 100% rename from .opencode/agent/glossary/pl.md rename to .opencode/glossary/pl.md diff --git a/.opencode/agent/glossary/ru.md b/.opencode/glossary/ru.md similarity index 100% rename from .opencode/agent/glossary/ru.md rename to .opencode/glossary/ru.md diff --git a/.opencode/agent/glossary/th.md b/.opencode/glossary/th.md similarity index 100% rename from .opencode/agent/glossary/th.md rename to .opencode/glossary/th.md diff --git a/.opencode/agent/glossary/zh-cn.md b/.opencode/glossary/zh-cn.md similarity index 100% rename from .opencode/agent/glossary/zh-cn.md rename to .opencode/glossary/zh-cn.md diff --git a/.opencode/agent/glossary/zh-tw.md b/.opencode/glossary/zh-tw.md similarity index 100% rename from .opencode/agent/glossary/zh-tw.md rename to .opencode/glossary/zh-tw.md diff --git a/bun.lock b/bun.lock index d68a9228fe77..27a2a9a2b632 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.10", + "version": "1.2.15", "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.10", + "version": "1.2.15", "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.10", + "version": "1.2.15", "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.10", + "version": "1.2.15", "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.10", + "version": "1.2.15", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.10", + "version": "1.2.15", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.10", + "version": "1.2.15", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.10", + "version": "1.2.15", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/infra/console.ts b/infra/console.ts index 283fe2c37cad..de72cb072eed 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -101,7 +101,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", }) const zenLiteProduct = new stripe.Product("ZenLite", { - name: "OpenCode Lite", + name: "OpenCode Go", }) const zenLitePrice = new stripe.Price("ZenLitePrice", { product: zenLiteProduct.id, diff --git a/package.json b/package.json index 2e7c1172aa64..3fd9f306676c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.9", + "packageManager": "bun@1.3.10", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts index f17557a800a8..74b3890888f6 100644 --- a/packages/app/e2e/projects/projects-switch.spec.ts +++ b/packages/app/e2e/projects/projects-switch.spec.ts @@ -9,7 +9,7 @@ import { sessionIDFromUrl, } from "../actions" import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" -import { createSdk, dirSlug } from "../utils" +import { createSdk, dirSlug, sessionPath } from "../utils" function slugFromUrl(url: string) { return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" @@ -51,7 +51,6 @@ test("switching back to a project opens the latest workspace session", async ({ const other = await createTestProject() const otherSlug = dirSlug(other) - const stamp = Date.now() let rootDir: string | undefined let workspaceDir: string | undefined let sessionID: string | undefined @@ -80,6 +79,7 @@ test("switching back to a project opens the latest workspace session", async ({ const workspaceSlug = slugFromUrl(page.url()) workspaceDir = base64Decode(workspaceSlug) + if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`) await openSidebar(page) const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first() @@ -92,15 +92,14 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`)) - const prompt = page.locator(promptSelector) - await expect(prompt).toBeVisible() - await prompt.fill(`project switch remembers workspace ${stamp}`) - await prompt.press("Enter") - - await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") - const created = sessionIDFromUrl(page.url()) - if (!created) throw new Error(`Failed to parse session id from URL: ${page.url()}`) + const created = await createSdk(workspaceDir) + .session.create() + .then((x) => x.data?.id) + if (!created) throw new Error(`Failed to create session for workspace: ${workspaceDir}`) sessionID = created + + await page.goto(sessionPath(workspaceDir, created)) + await expect(page.locator(promptSelector)).toBeVisible() await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) await openSidebar(page) @@ -114,7 +113,8 @@ test("switching back to a project opens the latest workspace session", async ({ await expect(rootButton).toBeVisible() await rootButton.click() - await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created) + await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`)) }, { extra: [other] }, ) diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts index 6bf7714a66d1..e9cfc03e4857 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { clearSessionDockSeed, seedSessionPermission, seedSessionQuestion, seedSessionTodos } from "../actions" +import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions" import { permissionDockSelector, promptSelector, @@ -11,11 +11,23 @@ import { } from "../selectors" type Sdk = Parameters[0] - -async function withDockSession(sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise) { - const session = await sdk.session.create({ title }).then((r) => r.data) +type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" } + +async function withDockSession( + sdk: Sdk, + title: string, + fn: (session: { id: string; title: string }) => Promise, + opts?: { permission?: PermissionRule[] }, +) { + const session = await sdk.session + .create(opts?.permission ? { title, permission: opts.permission } : { title }) + .then((r) => r.data) if (!session?.id) throw new Error("Session create did not return an id") - return fn(session) + try { + return await fn(session) + } finally { + await sdk.session.delete({ sessionID: session.id }).catch(() => undefined) + } } test.setTimeout(120_000) @@ -28,6 +40,85 @@ async function withDockSeed(sdk: Sdk, sessionID: string, fn: () => Promise } } +async function clearPermissionDock(page: any, label: RegExp) { + const dock = page.locator(permissionDockSelector) + for (let i = 0; i < 3; i++) { + const count = await dock.count() + if (count === 0) return + await dock.getByRole("button", { name: label }).click() + await page.waitForTimeout(150) + } +} + +async function withMockPermission( + page: any, + request: { + id: string + sessionID: string + permission: string + patterns: string[] + metadata?: Record + always?: string[] + }, + opts: { child?: any } | undefined, + fn: () => Promise, +) { + let pending = [ + { + ...request, + always: request.always ?? ["*"], + metadata: request.metadata ?? {}, + }, + ] + + const list = async (route: any) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(pending), + }) + } + + const reply = async (route: any) => { + const url = new URL(route.request().url()) + const id = url.pathname.split("/").pop() + pending = pending.filter((item) => item.id !== id) + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(true), + }) + } + + await page.route("**/permission", list) + await page.route("**/session/*/permissions/*", reply) + + const sessionList = opts?.child + ? async (route: any) => { + const res = await route.fetch() + const json = await res.json() + const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined + if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child) + await route.fulfill({ + status: res.status(), + headers: res.headers(), + contentType: "application/json", + body: JSON.stringify(json), + }) + } + : undefined + + if (sessionList) await page.route("**/session?*", sessionList) + + try { + return await fn() + } finally { + await page.unroute("**/permission", list) + await page.unroute("**/session/*/permissions/*", reply) + if (sessionList) await page.unroute("**/session?*", sessionList) + } +} + test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock default", async (session) => { await gotoSession(session.id) @@ -76,72 +167,175 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission once", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_once", sessionID: session.id, permission: "bash", - patterns: ["README.md"], - description: "Need permission for command", - }) - - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) - - await page - .locator(permissionDockSelector) - .getByRole("button", { name: /allow once/i }) - .click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + patterns: ["/tmp/opencode-e2e-perm-once"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) }) }) test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission reject", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_reject", sessionID: session.id, permission: "bash", - patterns: ["REJECT.md"], - }) - - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) - - await page.locator(permissionDockSelector).getByRole("button", { name: /deny/i }).click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + patterns: ["/tmp/opencode-e2e-perm-reject"], + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /deny/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) }) }) test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => { await withDockSession(sdk, "e2e composer dock permission always", async (session) => { - await withDockSeed(sdk, session.id, async () => { - await gotoSession(session.id) - - await seedSessionPermission(sdk, { + await gotoSession(session.id) + await withMockPermission( + page, + { + id: "per_e2e_always", sessionID: session.id, permission: "bash", - patterns: ["README.md"], - description: "Need permission for command", + patterns: ["/tmp/opencode-e2e-perm-always"], + metadata: { description: "Need permission for command" }, + }, + undefined, + async () => { + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow always/i) + await page.goto(page.url()) + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + }) +}) + +test("child session question request blocks parent dock and unblocks after submit", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child question parent", async (session) => { + await gotoSession(session.id) + + const child = await sdk.session + .create({ + title: "e2e composer dock child question", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withDockSeed(sdk, child.id, async () => { + await seedSessionQuestion(sdk, { + sessionID: child.id, + questions: [ + { + header: "Child input", + question: "Pick one child option", + options: [ + { label: "Continue", description: "Continue child" }, + { label: "Stop", description: "Stop child" }, + ], + }, + ], + }) + + const dock = page.locator(questionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() }) + } finally { + await sdk.session.delete({ sessionID: child.id }).catch(() => undefined) + } + }) +}) - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) - await expect(page.locator(promptSelector)).toHaveCount(0) +test("child session permission request blocks parent dock and supports allow once", async ({ + page, + sdk, + gotoSession, +}) => { + await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => { + await gotoSession(session.id) - await page - .locator(permissionDockSelector) - .getByRole("button", { name: /allow always/i }) - .click() - await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) - await expect(page.locator(promptSelector)).toBeVisible() - }) + const child = await sdk.session + .create({ + title: "e2e composer dock child permission", + parentID: session.id, + }) + .then((r) => r.data) + if (!child?.id) throw new Error("Child session create did not return an id") + + try { + await withMockPermission( + page, + { + id: "per_e2e_child", + sessionID: child.id, + permission: "bash", + patterns: ["/tmp/opencode-e2e-perm-child"], + metadata: { description: "Need child permission" }, + }, + { child }, + async () => { + await page.goto(page.url()) + const dock = page.locator(permissionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await clearPermissionDock(page, /allow once/i) + await page.goto(page.url()) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }, + ) + } finally { + await sdk.session.delete({ sessionID: child.id }).catch(() => undefined) + } }) }) diff --git a/packages/app/package.json b/packages/app/package.json index b9397b0f40de..446c14e9671e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.10", + "version": "1.2.15", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index af788d05b03c..5ca29a520a0c 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -97,9 +97,20 @@ export const DialogSelectModelUnpaid: Component = () => {
{i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
{language.t("dialog.provider.tag.recommended")} + + <> +
+ {language.t("dialog.provider.opencodeGo.tagline")} +
+ {language.t("dialog.provider.tag.recommended")} + +
{language.t("dialog.provider.anthropic.note")}
diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index 8bbd3054b9a2..76e718bb0011 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -29,6 +29,7 @@ export const DialogSelectProvider: Component = () => { if (id === "anthropic") return language.t("dialog.provider.anthropic.note") if (id === "openai") return language.t("dialog.provider.openai.note") if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") + if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") } return ( @@ -70,6 +71,9 @@ export const DialogSelectProvider: Component = () => {
{i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
{language.t("settings.providers.tag.custom")} @@ -77,6 +81,9 @@ export const DialogSelectProvider: Component = () => { {language.t("dialog.provider.tag.recommended")} {(value) =>
{value()}
}
+ + {language.t("dialog.provider.tag.recommended")} +
)} diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 825d1dab6cff..d531fa50ab60 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -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 { useParams } from "@solidjs/router" -import { useLayout } from "@/context/layout" import { useCommand } from "@/context/command" +import { useGlobalSDK } from "@/context/global-sdk" import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" 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,32 +45,67 @@ 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] @@ -213,7 +248,9 @@ export function SessionHeader() { const view = createMemo(() => layout.view(sessionKey)) const os = createMemo(() => detectOS(platform)) - const [exists, setExists] = createStore>>({ finder: true }) + const [exists, setExists] = createStore>>({ + finder: true, + }) const apps = createMemo(() => { if (os() === "macos") return MAC_APPS @@ -259,18 +296,34 @@ 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 - Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err)) + setOpenRequest("app", app) + platform + .openPath(directory, openWith) + .catch((err: unknown) => showRequestError(language, err)) + .finally(() => { + setOpenRequest("app", undefined) + }) } const copyPath = () => { @@ -315,7 +368,9 @@ export function SessionHeader() {
- {language.t("session.header.search.placeholder", { project: name() })} + {language.t("session.header.search.placeholder", { + project: name(), + })}
@@ -357,12 +412,21 @@ export function SessionHeader() {
@@ -377,7 +441,11 @@ export function SessionHeader() { as={IconButton} icon="chevron-down" variant="ghost" - class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover" + 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(), + }} aria-label={language.t("session.header.open.menu")} /> @@ -395,6 +463,7 @@ export function SessionHeader() { {(o) => ( { setMenu("open", false) openDir(o.id) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index d1837ee607db..55a25ca0c4b7 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -187,9 +187,22 @@ export const SettingsProviders: Component = () => {
{item.name} + + + {language.t("dialog.provider.opencode.tagline")} + + {language.t("dialog.provider.tag.recommended")} + + <> + + {language.t("dialog.provider.opencodeGo.tagline")} + + {language.t("dialog.provider.tag.recommended")} + +
{(key) => {language.t(key())}} diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 502364afdf3a..9ef5272ef548 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -3,7 +3,16 @@ import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { createMemo } from "solid-js" -export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] +export const popularProviders = [ + "opencode", + "opencode-go", + "anthropic", + "github-copilot", + "openai", + "google", + "openrouter", + "vercel", +] const popularProviderSet = new Set(popularProviders) export function useProviders() { diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 91a16b3b8532..e8964a664649 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "آخر", "dialog.provider.tag.recommended": "موصى به", "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", + "dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة", + "dialog.provider.opencodeGo.tagline": "اشتراك منخفض التكلفة للجميع", "dialog.provider.anthropic.note": "اتصل باستخدام Claude Pro/Max أو مفتاح API", "dialog.provider.copilot.note": "اتصل باستخدام Copilot أو مفتاح API", "dialog.provider.openai.note": "اتصل باستخدام ChatGPT Pro/Plus أو مفتاح API", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 7682a12b6972..f23668a0d875 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "Outro", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", + "dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis", + "dialog.provider.opencodeGo.tagline": "Assinatura de baixo custo para todos", "dialog.provider.anthropic.note": "Conectar com Claude Pro/Max ou chave de API", "dialog.provider.copilot.note": "Conectar com Copilot ou chave de API", "dialog.provider.openai.note": "Conectar com ChatGPT Pro/Plus ou chave de API", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index d658926268e2..6951f9db1f8e 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "Ostalo", "dialog.provider.tag.recommended": "Preporučeno", "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", + "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", + "dialog.provider.opencodeGo.tagline": "Povoljna pretplata za sve", "dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max", - "dialog.provider.copilot.note": "Claude modeli za pomoć pri kodiranju", + "dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot", "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", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index fabefcab7562..b870fb51a44b 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalet", "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", + "dialog.provider.opencode.tagline": "Pålidelige optimerede modeller", + "dialog.provider.opencodeGo.tagline": "Billigt abonnement for alle", "dialog.provider.anthropic.note": "Direkte adgang til Claude-modeller, inklusive Pro og Max", - "dialog.provider.copilot.note": "Claude-modeller til kodningsassistance", + "dialog.provider.copilot.note": "AI-modeller til kodningsassistance via GitHub Copilot", "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", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 3a7bbe927727..24d00a6813aa 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -95,6 +95,8 @@ export const dict = { "dialog.provider.group.other": "Andere", "dialog.provider.tag.recommended": "Empfohlen", "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", + "dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle", + "dialog.provider.opencodeGo.tagline": "Kostengünstiges Abo für alle", "dialog.provider.anthropic.note": "Mit Claude Pro/Max oder API-Schlüssel verbinden", "dialog.provider.copilot.note": "Mit Copilot oder API-Schlüssel verbinden", "dialog.provider.openai.note": "Mit ChatGPT Pro/Plus oder API-Schlüssel verbinden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 992509fcfa4e..bea29aa352e7 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "Other", "dialog.provider.tag.recommended": "Recommended", "dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more", + "dialog.provider.opencode.tagline": "Reliable optimized models", + "dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone", "dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max", - "dialog.provider.copilot.note": "Claude models for coding assistance", + "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", "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", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b55d54c0ca55..30c52c928fbd 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "Otro", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", + "dialog.provider.opencode.tagline": "Modelos optimizados y fiables", + "dialog.provider.opencodeGo.tagline": "Suscripción económica para todos", "dialog.provider.anthropic.note": "Acceso directo a modelos Claude, incluyendo Pro y Max", - "dialog.provider.copilot.note": "Modelos Claude para asistencia de codificación", + "dialog.provider.copilot.note": "Modelos de IA para asistencia de codificación a través de GitHub Copilot", "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", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index c961f060e1fd..3b690937e8a4 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "Autre", "dialog.provider.tag.recommended": "Recommandé", "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", + "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", + "dialog.provider.opencodeGo.tagline": "Abonnement abordable pour tous", "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", "dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API", "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 7a62c9de2716..c8a949e7822e 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -91,6 +91,8 @@ export const dict = { "dialog.provider.group.other": "その他", "dialog.provider.tag.recommended": "推奨", "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", + "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", + "dialog.provider.opencodeGo.tagline": "すべての人に低価格のサブスクリプション", "dialog.provider.anthropic.note": "Claude Pro/MaxまたはAPIキーで接続", "dialog.provider.copilot.note": "CopilotまたはAPIキーで接続", "dialog.provider.openai.note": "ChatGPT Pro/PlusまたはAPIキーで接続", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 8967c71cff7d..d5cedc7deaac 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -95,6 +95,8 @@ export const dict = { "dialog.provider.group.other": "기타", "dialog.provider.tag.recommended": "추천", "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", + "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", + "dialog.provider.opencodeGo.tagline": "모두를 위한 저렴한 구독", "dialog.provider.anthropic.note": "Claude Pro/Max 또는 API 키로 연결", "dialog.provider.copilot.note": "Copilot 또는 API 키로 연결", "dialog.provider.openai.note": "ChatGPT Pro/Plus 또는 API 키로 연결", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 8e1b1ce629dc..02a73def023d 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -102,8 +102,10 @@ export const dict = { "dialog.provider.group.other": "Andre", "dialog.provider.tag.recommended": "Anbefalt", "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", + "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", + "dialog.provider.opencodeGo.tagline": "Rimelig abonnement for alle", "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", - "dialog.provider.copilot.note": "Claude-modeller for kodeassistanse", + "dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot", "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", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 9b924fd642ea..587698e68936 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -91,8 +91,10 @@ export const dict = { "dialog.provider.group.other": "Inne", "dialog.provider.tag.recommended": "Zalecane", "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", + "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", + "dialog.provider.opencodeGo.tagline": "Tania subskrypcja dla każdego", "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", - "dialog.provider.copilot.note": "Modele Claude do pomocy w kodowaniu", + "dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot", "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", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index cf02285821e4..4dc5007a6e93 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "Другие", "dialog.provider.tag.recommended": "Рекомендуемые", "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", + "dialog.provider.opencode.tagline": "Надежные оптимизированные модели", + "dialog.provider.opencodeGo.tagline": "Доступная подписка для всех", "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", - "dialog.provider.copilot.note": "Модели Claude для помощи в кодировании", + "dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot", "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", "dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов", "dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 1b8abe953b77..831cfe598f39 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -99,8 +99,10 @@ export const dict = { "dialog.provider.group.other": "อื่น ๆ", "dialog.provider.tag.recommended": "แนะนำ", "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", + "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", + "dialog.provider.opencodeGo.tagline": "การสมัครสมาชิกราคาประหยัดสำหรับทุกคน", "dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max", - "dialog.provider.copilot.note": "โมเดล Claude สำหรับการช่วยเหลือในการเขียนโค้ด", + "dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot", "dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ", "dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง", "dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 62c7bb9ff233..9cda1058481f 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -126,6 +126,8 @@ export const dict = { "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推荐", "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", + "dialog.provider.opencode.tagline": "可靠的优化模型", + "dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接", "dialog.provider.copilot.note": "使用 Copilot 或 API 密钥连接", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 密钥连接", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index cb8f068f63b7..69f963085ede 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -103,6 +103,8 @@ export const dict = { "dialog.provider.group.other": "其他", "dialog.provider.tag.recommended": "推薦", "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", + "dialog.provider.opencode.tagline": "可靠的優化模型", + "dialog.provider.opencodeGo.tagline": "適合所有人的低成本訂閱", "dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 金鑰連線", "dialog.provider.openai.note": "使用 ChatGPT Pro/Plus 或 API 金鑰連線", "dialog.provider.copilot.note": "使用 Copilot 或 API 金鑰連線", diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 4f1d93ab2829..3daab6b256de 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,12 +1,11 @@ import { createEffect, createMemo, Show, type ParentProps } from "solid-js" import { createStore } from "solid-js/store" import { useNavigate, useParams } from "@solidjs/router" -import { SDKProvider, useSDK } from "@/context/sdk" +import { SDKProvider } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" import { DataProvider } from "@opencode-ai/ui/context" -import type { QuestionAnswer } from "@opencode-ai/sdk/v2" import { decode64 } from "@/utils/base64" import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" @@ -15,21 +14,20 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { const params = useParams() const navigate = useNavigate() const sync = useSync() - const sdk = useSDK() return ( sdk.client.permission.respond(input)} - onQuestionReply={(input: { requestID: string; answers: QuestionAnswer[] }) => sdk.client.question.reply(input)} - onQuestionReject={(input: { requestID: string }) => sdk.client.question.reject(input)} onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)} onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`} + onOpenFilePath={(input) => { + window.dispatchEvent( + new CustomEvent("opencode:open-file-path", { + detail: input, + }), + ) + }} > {props.children} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 62094a6e4282..cb194052d1e0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -61,6 +61,7 @@ import { displayName, errorMessage, getDraggableId, + latestRootSession, sortedRootSessions, syncWorkspaceOrder, workspaceKey, @@ -1093,14 +1094,51 @@ export default function Layout(props: ParentProps) { return meta?.worktree ?? directory } - function navigateToProject(directory: string | undefined) { + async function navigateToProject(directory: string | undefined) { if (!directory) return const root = projectRoot(directory) server.projects.touch(root) + const project = layout.projects.list().find((item) => item.worktree === root) + const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])])) + const openSession = async (target: { directory: string; id: string }) => { + const resolved = await globalSDK.client.session + .get({ sessionID: target.id }) + .then((x) => x.data) + .catch(() => undefined) + const next = resolved?.directory ? resolved : target + setStore("lastProjectSession", root, { directory: next.directory, id: next.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(next.directory)}/session/${next.id}`) + } const projectSession = store.lastProjectSession[root] if (projectSession?.id) { - navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`) + await openSession(projectSession) + return + } + + const latest = latestRootSession( + dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]), + Date.now(), + ) + if (latest) { + await openSession(latest) + return + } + + const fetched = latestRootSession( + await Promise.all( + dirs.map(async (item) => ({ + path: { directory: item }, + session: await globalSDK.client.session + .list({ directory: item }) + .then((x) => x.data ?? []) + .catch(() => []), + })), + ), + Date.now(), + ) + if (fetched) { + await openSession(fetched) return } diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 83d8f4748aba..7627d9ba17c6 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -1,6 +1,25 @@ import { describe, expect, test } from "bun:test" +import { type Session } from "@opencode-ai/sdk/v2/client" import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links" -import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" +import { + displayName, + errorMessage, + getDraggableId, + latestRootSession, + syncWorkspaceOrder, + workspaceKey, +} from "./helpers" + +const session = (input: Partial & Pick) => + ({ + title: "", + version: "v2", + parentID: undefined, + messageCount: 0, + permissions: { session: {}, share: {} }, + time: { created: 0, updated: 0, archived: undefined }, + ...input, + }) as Session describe("layout deep links", () => { test("parses open-project deep links", () => { @@ -73,6 +92,61 @@ describe("layout workspace helpers", () => { expect(result).toEqual(["/root", "/c", "/b"]) }) + test("finds the latest root session across workspaces", () => { + const result = latestRootSession( + [ + { + path: { directory: "/root" }, + session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })], + }, + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "workspace", + directory: "/workspace", + time: { created: 2, updated: 2, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("workspace") + }) + + test("ignores archived and child sessions when finding latest root session", () => { + const result = latestRootSession( + [ + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "archived", + directory: "/workspace", + time: { created: 10, updated: 10, archived: 10 }, + }), + session({ + id: "child", + directory: "/workspace", + parentID: "parent", + time: { created: 20, updated: 20, archived: undefined }, + }), + session({ + id: "root", + directory: "/workspace", + time: { created: 30, updated: 30, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("root") + }) + test("extracts draggable id safely", () => { expect(getDraggableId({ draggable: { id: "x" } })).toBe("x") expect(getDraggableId({ draggable: { id: 42 } })).toBeUndefined() diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 6a1e7c0123d8..be4297fbe914 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -28,6 +28,11 @@ export const isRootVisibleSession = (session: Session, directory: string) => export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now)) +export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) => + stores + .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory))) + .sort(sortSessions(now))[0] + export const childMapByParent = (sessions: Session[]) => { const map = new Map() for (const session of sessions) { diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index e19e6f430f0d..3c3652e38f36 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,4 +1,5 @@ -import { createEffect, createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js" +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" import { base64Encode } from "@opencode-ai/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" @@ -7,7 +8,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { createSortable } from "@thisbeyond/solid-dnd" -import { type LocalProject } from "@/context/layout" +import { useLayout, type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" @@ -60,6 +61,7 @@ const ProjectTile = (props: { selected: Accessor active: Accessor overlay: Accessor + suppressHover: Accessor dirs: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void @@ -71,9 +73,11 @@ const ProjectTile = (props: { closeProject: (directory: string) => void setMenu: (value: boolean) => void setOpen: (value: boolean) => void + setSuppressHover: (value: boolean) => void language: ReturnType }): JSX.Element => { const notification = useNotification() + const layout = useLayout() const unseenCount = createMemo(() => props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) @@ -107,17 +111,28 @@ const ProjectTile = (props: { }} onMouseEnter={(event: MouseEvent) => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectMouseEnter(props.project.worktree, event) }} onMouseLeave={() => { + if (props.suppressHover()) props.setSuppressHover(false) if (!props.overlay()) return props.onProjectMouseLeave(props.project.worktree) }} onFocus={() => { if (!props.overlay()) return + if (props.suppressHover()) return props.onProjectFocus(props.project.worktree) }} - onClick={() => props.navigateToProject(props.project.worktree)} + onClick={() => { + if (props.selected()) { + props.setSuppressHover(true) + layout.sidebar.toggle() + return + } + props.setSuppressHover(false) + props.navigateToProject(props.project.worktree) + }} onBlur={() => props.setOpen(false)} > @@ -278,16 +293,19 @@ export const SortableProject = (props: { const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) - const [open, setOpen] = createSignal(false) - const [menu, setMenu] = createSignal(false) + const [state, setState] = createStore({ + open: false, + menu: false, + suppressHover: false, + }) const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) const active = createMemo(() => projectTileActive({ - menu: menu(), + menu: state.menu, preview: preview(), - open: open(), + open: state.open, overlay: overlay(), hoverProject: props.ctx.hoverProject(), worktree: props.project.worktree, @@ -296,8 +314,14 @@ export const SortableProject = (props: { createEffect(() => { if (preview()) return - if (!open()) return - setOpen(false) + if (!state.open) return + setState("open", false) + }) + + createEffect(() => { + if (!selected()) return + if (!state.open) return + setState("open", false) }) const label = (directory: string) => { @@ -328,6 +352,7 @@ export const SortableProject = (props: { selected={selected} active={active} overlay={overlay} + suppressHover={() => state.suppressHover} dirs={dirs} onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseLeave={props.ctx.onProjectMouseLeave} @@ -337,8 +362,9 @@ export const SortableProject = (props: { toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} - setMenu={setMenu} - setOpen={setOpen} + setMenu={(value) => setState("menu", value)} + setOpen={(value) => setState("open", value)} + setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> ) @@ -346,17 +372,18 @@ export const SortableProject = (props: { return ( // @ts-ignore
- + { - if (menu()) return - setOpen(value) + if (state.menu) return + if (value && state.suppressHover) return + setState("open", value) if (value) props.ctx.setHoverSession(undefined) }} > @@ -371,7 +398,7 @@ export const SortableProject = (props: { projectChildren={projectChildren} workspaceSessions={workspaceSessions} workspaceChildren={workspaceChildren} - setOpen={setOpen} + setOpen={(value) => setState("open", value)} ctx={props.ctx} language={language} /> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index e0ef92682d94..779cbcbd58e6 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -254,12 +254,13 @@ export default function Page() { const msgs = visibleUserMessages() if (msgs.length === 0) 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 + 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 - if (targetIndex === msgs.length - 1) { + if (targetIndex === msgs.length) { resumeScroll() return } @@ -415,7 +416,7 @@ export default function Page() { ) const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) + const reviewTab = createMemo(() => isDesktop()) const fileTreeTab = () => layout.fileTree.tab() const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) @@ -456,6 +457,18 @@ export default function Page() { loadFile: file.load, }) + onMount(() => { + const open = (event: Event) => { + const detail = (event as CustomEvent<{ path?: string }>).detail + const path = detail?.path + if (!path) return + openReviewFile(path) + } + + window.addEventListener("opencode:open-file-path", open) + onCleanup(() => window.removeEventListener("opencode:open-file-path", open)) + }) + const changesOptions = ["session", "turn"] as const const changesOptionsList = [...changesOptions] @@ -699,33 +712,12 @@ export default function Page() { const active = tabs().active() const tab = active === "review" || (!active && hasReview()) ? "changes" : "all" layout.fileTree.setTab(tab) - return } - - if (fileTreeTab() !== "changes") return - tabs().setActive("review") }, { defer: true }, ), ) - createEffect(() => { - if (!isDesktop()) return - if (!layout.fileTree.opened()) return - if (fileTreeTab() !== "all") return - - const active = tabs().active() - if (active && active !== "review") return - - const first = openedTabs()[0] - if (first) { - tabs().setActive(first) - return - } - - if (contextOpen()) tabs().setActive("context") - }) - createEffect(() => { const id = params.id if (!id) return diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts new file mode 100644 index 000000000000..7b6029eb31b7 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-composer-state.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test" +import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" +import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" + +const session = (input: { id: string; parentID?: string }) => + ({ + id: input.id, + parentID: input.parentID, + }) as Session + +const permission = (id: string, sessionID: string) => + ({ + id, + sessionID, + }) as PermissionRequest + +const question = (id: string, sessionID: string) => + ({ + id, + sessionID, + questions: [], + }) as QuestionRequest + +describe("sessionPermissionRequest", () => { + test("prefers the current session permission", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const permissions = { + root: [permission("perm-root", "root")], + child: [permission("perm-child", "child")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-root") + }) + + test("returns a nested child permission", () => { + const sessions = [ + session({ id: "root" }), + session({ id: "child", parentID: "root" }), + session({ id: "grand", parentID: "child" }), + session({ id: "other" }), + ] + const permissions = { + grand: [permission("perm-grand", "grand")], + other: [permission("perm-other", "other")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-grand") + }) + + test("returns undefined without a matching tree permission", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const permissions = { + other: [permission("perm-other", "other")], + } + + expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined() + }) +}) + +describe("sessionQuestionRequest", () => { + test("prefers the current session question", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const questions = { + root: [question("q-root", "root")], + child: [question("q-child", "child")], + } + + expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-root") + }) + + test("returns a nested child question", () => { + const sessions = [ + session({ id: "root" }), + session({ id: "child", parentID: "root" }), + session({ id: "grand", parentID: "child" }), + ] + const questions = { + grand: [question("q-grand", "grand")], + } + + expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand") + }) +}) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 04c6f7e692a5..ed65867ef00e 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -7,14 +7,20 @@ import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" +import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree" export function createSessionComposerBlocked() { const params = useParams() const sync = useSync() + const permissionRequest = createMemo(() => + sessionPermissionRequest(sync.data.session, sync.data.permission, params.id), + ) + const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id)) + return createMemo(() => { const id = params.id if (!id) return false - return !!sync.data.permission[id]?.[0] || !!sync.data.question[id]?.[0] + return !!permissionRequest() || !!questionRequest() }) } @@ -26,18 +32,18 @@ export function createSessionComposerState() { const language = useLanguage() const questionRequest = createMemo((): QuestionRequest | undefined => { - const id = params.id - if (!id) return - return sync.data.question[id]?.[0] + return sessionQuestionRequest(sync.data.session, sync.data.question, params.id) }) const permissionRequest = createMemo((): PermissionRequest | undefined => { - const id = params.id - if (!id) return - return sync.data.permission[id]?.[0] + return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id) }) - const blocked = createSessionComposerBlocked() + const blocked = createMemo(() => { + const id = params.id + if (!id) return false + return !!permissionRequest() || !!questionRequest() + }) const todos = createMemo((): Todo[] => { const id = params.id diff --git a/packages/app/src/pages/session/composer/session-request-tree.ts b/packages/app/src/pages/session/composer/session-request-tree.ts new file mode 100644 index 000000000000..f9673e254944 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-request-tree.ts @@ -0,0 +1,45 @@ +import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client" + +function sessionTreeRequest(session: Session[], request: Record, sessionID?: string) { + if (!sessionID) return + + const map = session.reduce((acc, item) => { + if (!item.parentID) return acc + const list = acc.get(item.parentID) + if (list) list.push(item.id) + if (!list) acc.set(item.parentID, [item.id]) + return acc + }, new Map()) + + const seen = new Set([sessionID]) + const ids = [sessionID] + for (const id of ids) { + const list = map.get(id) + if (!list) continue + for (const child of list) { + if (seen.has(child)) continue + seen.add(child) + ids.push(child) + } + } + + const id = ids.find((id) => !!request[id]?.[0]) + if (!id) return + return request[id]?.[0] +} + +export function sessionPermissionRequest( + session: Session[], + request: Record, + sessionID?: string, +) { + return sessionTreeRequest(session, request, sessionID) +} + +export function sessionQuestionRequest( + session: Session[], + request: Record, + sessionID?: string, +) { + return sessionTreeRequest(session, request, sessionID) +} diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 615d1a0bea4d..b84109035507 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -376,6 +376,7 @@ export function MessageTimeline(props: { >
isDesktop() && view().reviewPanel.opened()) const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened())) - const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) + const reviewTab = createMemo(() => isDesktop()) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) @@ -202,133 +202,124 @@ export function SessionSidePanel(props: { >
- - - - -
- { - const stop = createFileTabListSync({ el, contextOpen }) - onCleanup(stop) - }} - > - - -
-
{language.t("session.tab.review")}
- -
- {reviewCount()} -
-
-
-
-
- - - tabs().close("context")} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close("context")} - > -
- -
{language.t("session.tab.context")}
+ + + + +
+ { + const stop = createFileTabListSync({ el, contextOpen }) + onCleanup(stop) + }} + > + + +
+
{language.t("session.tab.review")}
+ +
+ {reviewCount()}
- -
- - {(tab) => } - - - + +
+
+
+ + - dialog.show(() => ) - } - aria-label={language.t("command.file.open")} + class="h-5 w-5" + onClick={() => tabs().close("context")} + aria-label={language.t("common.closeTab")} /> - - -
-
- - - - {props.reviewPanel()} - + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
- - - -
-
- -
- {language.t("session.files.selectToOpen")} -
-
+ + {(tab) => } + + + + dialog.show(() => )} + aria-label={language.t("command.file.open")} + /> + + + +
+ + + + {props.reviewPanel()} + + + + + +
+
+ +
+ {language.t("session.files.selectToOpen")}
- - +
+
+
+
- - - -
- -
-
-
+ + + +
+ +
+
+
- - {(tab) => } - -
- - - {(tab) => { - const path = createMemo(() => file.pathFromTab(tab)) - return ( -
- {(p) => } -
- ) - }} -
-
-
- } - > - {props.reviewPanel()} - + + {(tab) => } + + + + + {(tab) => { + const path = createMemo(() => file.pathFromTab(tab)) + return ( +
+ {(p) => } +
+ ) + }} +
+
+
diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts index 555761ad1ec9..b704e460bc00 100644 --- a/packages/app/src/pages/session/use-session-hash-scroll.ts +++ b/packages/app/src/pages/session/use-session-hash-scroll.ts @@ -45,7 +45,9 @@ export const useSessionHashScroll = (input: { const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() - const top = a.top - b.top + root.scrollTop + 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) root.scrollTo({ top, behavior }) return true } diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 395feeb4af5c..ad5813ced67d 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,13 +1,13 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.10", + "version": "1.2.15", "type": "module", "license": "MIT", "scripts": { "typecheck": "tsgo --noEmit", "dev": "vite dev --host 0.0.0.0", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", - "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json", + "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json", "start": "vite start" }, "dependencies": { diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 36c86ef101e1..cda1e2a36375 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -344,8 +344,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "كتابة الكاش", "workspace.usage.breakdown.output": "الخرج", "workspace.usage.breakdown.reasoning": "المنطق", - "workspace.usage.subscription": "الاشتراك (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "التكلفة", @@ -491,21 +491,26 @@ export const dict = { "workspace.lite.time.minute": "دقيقة", "workspace.lite.time.minutes": "دقائق", "workspace.lite.time.fewSeconds": "بضع ثوان", - "workspace.lite.subscription.title": "اشتراك Lite", - "workspace.lite.subscription.message": "أنت مشترك في OpenCode Lite.", + "workspace.lite.subscription.title": "اشتراك Go", + "workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.", "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.other.title": "اشتراك Lite", + "workspace.lite.subscription.selectProvider": + 'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.', + "workspace.lite.other.title": "اشتراك Go", "workspace.lite.other.message": - "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Lite. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", - "workspace.lite.promo.title": "OpenCode Lite", + "عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "احصل على وصول إلى أفضل النماذج المفتوحة — Kimi K2.5، و GLM-5، و MiniMax M2.5 — مع حدود استخدام سخية مقابل $10 شهريًا.", - "workspace.lite.promo.subscribe": "الاشتراك في Lite", + "OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.", + "workspace.lite.promo.modelsTitle": "ما يتضمنه", + "workspace.lite.promo.footer": + "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.", + "workspace.lite.promo.subscribe": "الاشتراك في Go", "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", "download.title": "OpenCode | تنزيل", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 5367a748bfeb..ddeb0c10a94c 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -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": "assinatura (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Custo", @@ -497,21 +497,26 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minutos", "workspace.lite.time.fewSeconds": "alguns segundos", - "workspace.lite.subscription.title": "Assinatura Lite", - "workspace.lite.subscription.message": "Você assina o OpenCode Lite.", + "workspace.lite.subscription.title": "Assinatura Go", + "workspace.lite.subscription.message": "Você assina o OpenCode Go.", "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.other.title": "Assinatura Lite", + "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.message": - "Outro membro neste workspace já assina o OpenCode Lite. Apenas um membro por workspace pode assinar.", - "workspace.lite.promo.title": "OpenCode Lite", + "Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Redirecionando...", "download.title": "OpenCode | Baixar", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 2f1be69cadd9..18b2b89ff95d 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -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": "abonnement (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Omkostninger", @@ -495,21 +495,26 @@ export const dict = { "workspace.lite.time.minute": "minut", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "et par sekunder", - "workspace.lite.subscription.title": "Lite-abonnement", - "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "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.other.title": "Lite-abonnement", + "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.message": - "Et andet medlem i dette workspace abonnerer allerede på OpenCode Lite. Kun ét medlem pr. workspace kan abonnere.", - "workspace.lite.promo.title": "OpenCode Lite", + "Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Omdirigerer...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 49df65f8dc0b..5bee74aed39b 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -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": "Abonnement (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Kosten", @@ -497,21 +497,26 @@ export const dict = { "workspace.lite.time.minute": "Minute", "workspace.lite.time.minutes": "Minuten", "workspace.lite.time.fewSeconds": "einige Sekunden", - "workspace.lite.subscription.title": "Lite-Abonnement", - "workspace.lite.subscription.message": "Du hast OpenCode Lite abonniert.", + "workspace.lite.subscription.title": "Go-Abonnement", + "workspace.lite.subscription.message": "Du hast OpenCode Go 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.other.title": "Lite-Abonnement", + "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.message": - "Ein anderes Mitglied in diesem Workspace hat OpenCode Lite bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", - "workspace.lite.promo.title": "OpenCode Lite", + "Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Leite weiter...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 42b88dd16e5d..d6db2e7f8af6 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -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": "subscription (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Cost", @@ -489,21 +489,26 @@ export const dict = { "workspace.lite.time.minute": "minute", "workspace.lite.time.minutes": "minutes", "workspace.lite.time.fewSeconds": "a few seconds", - "workspace.lite.subscription.title": "Lite Subscription", - "workspace.lite.subscription.message": "You are subscribed to OpenCode Lite.", + "workspace.lite.subscription.title": "Go Subscription", + "workspace.lite.subscription.message": "You are subscribed to OpenCode Go.", "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.other.title": "Lite Subscription", + "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.message": - "Another member in this workspace is already subscribed to OpenCode Lite. Only one member per workspace can subscribe.", - "workspace.lite.promo.title": "OpenCode Lite", + "Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Redirecting...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index f4ac1cc63739..c4676fe6e9a3 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -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": "suscripción (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Costo", @@ -498,21 +498,26 @@ 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 Lite", - "workspace.lite.subscription.message": "Estás suscrito a OpenCode Lite.", + "workspace.lite.subscription.title": "Suscripción Go", + "workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.", "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.other.title": "Suscripción Lite", + "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.message": - "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", + "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", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Redirigiendo...", "download.title": "OpenCode | Descargar", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 05ee4e843528..7b2306bc2f69 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -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": "abonnement ({{amount}} $)", - "workspace.usage.lite": "lite ({{amount}} $)", + "workspace.usage.subscription": "Black ({{amount}} $)", + "workspace.usage.lite": "Go ({{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 Lite", - "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Lite.", + "workspace.lite.subscription.title": "Abonnement Go", + "workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.", "workspace.lite.subscription.manage": "Gérer l'abonnement", "workspace.lite.subscription.rollingUsage": "Utilisation glissante", "workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire", @@ -515,13 +515,18 @@ 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.other.title": "Abonnement Lite", + "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.message": - "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", + "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", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Redirection...", "download.title": "OpenCode | Téléchargement", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 08b7955047d1..81dd293057fb 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -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": "abbonamento (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Costo", @@ -497,21 +497,26 @@ export const dict = { "workspace.lite.time.minute": "minuto", "workspace.lite.time.minutes": "minuti", "workspace.lite.time.fewSeconds": "pochi secondi", - "workspace.lite.subscription.title": "Abbonamento Lite", - "workspace.lite.subscription.message": "Sei abbonato a OpenCode Lite.", + "workspace.lite.subscription.title": "Abbonamento Go", + "workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.", "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.other.title": "Abbonamento Lite", + "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.message": - "Un altro membro in questo workspace è già abbonato a OpenCode Lite. Solo un membro per workspace può abbonarsi.", - "workspace.lite.promo.title": "OpenCode Lite", + "Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Reindirizzamento...", "download.title": "OpenCode | Download", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 2c8e9d6b4d00..afcf6aeb9b31 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -346,8 +346,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "キャッシュ書き込み", "workspace.usage.breakdown.output": "出力", "workspace.usage.breakdown.reasoning": "推論", - "workspace.usage.subscription": "サブスクリプション (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "コスト", @@ -495,21 +495,26 @@ export const dict = { "workspace.lite.time.minute": "分", "workspace.lite.time.minutes": "分", "workspace.lite.time.fewSeconds": "数秒", - "workspace.lite.subscription.title": "Liteサブスクリプション", - "workspace.lite.subscription.message": "あなたは OpenCode Lite を購読しています。", + "workspace.lite.subscription.title": "Goサブスクリプション", + "workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。", "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.other.title": "Liteサブスクリプション", + "workspace.lite.subscription.selectProvider": + "Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。", + "workspace.lite.other.title": "Goサブスクリプション", "workspace.lite.other.message": - "このワークスペースの別のメンバーが既に OpenCode Lite を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", - "workspace.lite.promo.title": "OpenCode Lite", + "このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "月額$10で、十分な利用枠が設けられた最高のオープンモデル — Kimi K2.5、GLM-5、および MiniMax M2.5 — にアクセスできます。", - "workspace.lite.promo.subscribe": "Liteを購読する", + "OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠で提供します。", + "workspace.lite.promo.modelsTitle": "含まれるもの", + "workspace.lite.promo.footer": + "このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。", + "workspace.lite.promo.subscribe": "Goを購読する", "workspace.lite.promo.subscribing": "リダイレクト中...", "download.title": "OpenCode | ダウンロード", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 8f4e58e7de42..b2375c3f7d8b 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -343,8 +343,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "캐시 쓰기", "workspace.usage.breakdown.output": "출력", "workspace.usage.breakdown.reasoning": "추론", - "workspace.usage.subscription": "구독 (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "비용", @@ -490,21 +490,26 @@ export const dict = { "workspace.lite.time.minute": "분", "workspace.lite.time.minutes": "분", "workspace.lite.time.fewSeconds": "몇 초", - "workspace.lite.subscription.title": "Lite 구독", - "workspace.lite.subscription.message": "현재 OpenCode Lite를 구독 중입니다.", + "workspace.lite.subscription.title": "Go 구독", + "workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.", "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.other.title": "Lite 구독", + "workspace.lite.subscription.selectProvider": + 'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.', + "workspace.lite.other.title": "Go 구독", "workspace.lite.other.message": - "이 워크스페이스의 다른 멤버가 이미 OpenCode Lite를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", - "workspace.lite.promo.title": "OpenCode Lite", + "이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "월 $10의 넉넉한 사용 한도로 최고의 오픈 모델인 Kimi K2.5, GLM-5, MiniMax M2.5에 액세스하세요.", - "workspace.lite.promo.subscribe": "Lite 구독하기", + "OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.", + "workspace.lite.promo.modelsTitle": "포함 내역", + "workspace.lite.promo.footer": + "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.", + "workspace.lite.promo.subscribe": "Go 구독하기", "workspace.lite.promo.subscribing": "리디렉션 중...", "download.title": "OpenCode | 다운로드", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index e5bfef989dd1..41bacfb053ff 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -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": "abonnement (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Kostnad", @@ -495,21 +495,26 @@ export const dict = { "workspace.lite.time.minute": "minutt", "workspace.lite.time.minutes": "minutter", "workspace.lite.time.fewSeconds": "noen få sekunder", - "workspace.lite.subscription.title": "Lite-abonnement", - "workspace.lite.subscription.message": "Du abonnerer på OpenCode Lite.", + "workspace.lite.subscription.title": "Go-abonnement", + "workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.", "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.other.title": "Lite-abonnement", + "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.message": - "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", + "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", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Omdirigerer...", "download.title": "OpenCode | Last ned", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index c2f9b3712ef9..dc3c59196d56 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -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": "subskrypcja (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Koszt", @@ -496,21 +496,26 @@ 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 Lite", - "workspace.lite.subscription.message": "Subskrybujesz OpenCode Lite.", + "workspace.lite.subscription.title": "Subskrypcja Go", + "workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.", "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.other.title": "Subskrypcja Lite", + "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.message": - "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", + "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", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Przekierowywanie...", "download.title": "OpenCode | Pobierz", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 3bedf80b5ba4..21b89dc94e05 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -353,8 +353,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "Запись кэша", "workspace.usage.breakdown.output": "Выход", "workspace.usage.breakdown.reasoning": "Reasoning (рассуждения)", - "workspace.usage.subscription": "подписка (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Расходы", @@ -501,21 +501,26 @@ export const dict = { "workspace.lite.time.minute": "минута", "workspace.lite.time.minutes": "минут", "workspace.lite.time.fewSeconds": "несколько секунд", - "workspace.lite.subscription.title": "Подписка Lite", - "workspace.lite.subscription.message": "Вы подписаны на OpenCode Lite.", + "workspace.lite.subscription.title": "Подписка Go", + "workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.", "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.other.title": "Подписка Lite", + "workspace.lite.subscription.selectProvider": + 'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.', + "workspace.lite.other.title": "Подписка Go", "workspace.lite.other.message": - "Другой участник в этом рабочем пространстве уже подписан на OpenCode Lite. Только один участник в рабочем пространстве может оформить подписку.", - "workspace.lite.promo.title": "OpenCode Lite", + "Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "Получите доступ к лучшим открытым моделям — Kimi K2.5, GLM-5 и MiniMax M2.5 — с щедрыми лимитами использования за $10 в месяц.", - "workspace.lite.promo.subscribe": "Подписаться на Lite", + "OpenCode Go — это подписка за $10 в месяц, которая предоставляет надежный доступ к популярным открытым моделям для кодинга с щедрыми лимитами использования.", + "workspace.lite.promo.modelsTitle": "Что включено", + "workspace.lite.promo.footer": + "План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.", + "workspace.lite.promo.subscribe": "Подписаться на Go", "workspace.lite.promo.subscribing": "Перенаправление...", "download.title": "OpenCode | Скачать", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 3d36dcbb2272..0646483544fb 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -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": "สมัครสมาชิก (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "ค่าใช้จ่าย", @@ -494,21 +494,26 @@ export const dict = { "workspace.lite.time.minute": "นาที", "workspace.lite.time.minutes": "นาที", "workspace.lite.time.fewSeconds": "ไม่กี่วินาที", - "workspace.lite.subscription.title": "การสมัครสมาชิก Lite", - "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Lite แล้ว", + "workspace.lite.subscription.title": "การสมัครสมาชิก Go", + "workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว", "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.other.title": "การสมัครสมาชิก Lite", + "workspace.lite.subscription.selectProvider": + 'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go', + "workspace.lite.other.title": "การสมัครสมาชิก Go", "workspace.lite.other.message": - "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Lite แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", - "workspace.lite.promo.title": "OpenCode Lite", + "สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น", + "workspace.lite.promo.title": "OpenCode Go", "workspace.lite.promo.description": - "เข้าถึงโมเดลเปิดที่ดีที่สุด — Kimi K2.5, GLM-5 และ MiniMax M2.5 — พร้อมขีดจำกัดการใช้งานมากมายในราคา $10 ต่อเดือน", - "workspace.lite.promo.subscribe": "สมัครสมาชิก Lite", + "OpenCode Go เป็นการสมัครสมาชิกราคา 10 ดอลลาร์ต่อเดือน ที่ให้การเข้าถึงโมเดลโอเพนโค้ดดิงยอดนิยมได้อย่างเสถียร ด้วยขีดจำกัดการใช้งานที่ครอบคลุม", + "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", + "workspace.lite.promo.footer": + "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ", + "workspace.lite.promo.subscribe": "สมัครสมาชิก Go", "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", "download.title": "OpenCode | ดาวน์โหลด", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index bfa7d09aef3f..d94dd15d0df5 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -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": "abonelik (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "Maliyet", @@ -497,21 +497,26 @@ export const dict = { "workspace.lite.time.minute": "dakika", "workspace.lite.time.minutes": "dakika", "workspace.lite.time.fewSeconds": "birkaç saniye", - "workspace.lite.subscription.title": "Lite Aboneliği", - "workspace.lite.subscription.message": "OpenCode Lite abonesisiniz.", + "workspace.lite.subscription.title": "Go Aboneliği", + "workspace.lite.subscription.message": "OpenCode Go 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.other.title": "Lite Aboneliği", + "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.message": - "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", + "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", "workspace.lite.promo.description": - "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", + "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", "workspace.lite.promo.subscribing": "Yönlendiriliyor...", "download.title": "OpenCode | İndir", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 2c41be7cf7c9..bf21073ce1ff 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -334,8 +334,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "缓存写入", "workspace.usage.breakdown.output": "输出", "workspace.usage.breakdown.reasoning": "推理", - "workspace.usage.subscription": "订阅 (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "成本", @@ -481,20 +481,25 @@ export const dict = { "workspace.lite.time.minute": "分钟", "workspace.lite.time.minutes": "分钟", "workspace.lite.time.fewSeconds": "几秒钟", - "workspace.lite.subscription.title": "Lite 订阅", - "workspace.lite.subscription.message": "您已订阅 OpenCode Lite。", + "workspace.lite.subscription.title": "Go 订阅", + "workspace.lite.subscription.message": "您已订阅 OpenCode Go。", "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.other.title": "Lite 订阅", - "workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Lite。每个工作区只有一名成员可以订阅。", - "workspace.lite.promo.title": "OpenCode Lite", + "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.promo.description": - "每月仅需 $10 即可访问最优秀的开源模型 — Kimi K2.5, GLM-5, 和 MiniMax M2.5 — 并享受充裕的使用限额。", - "workspace.lite.promo.subscribe": "订阅 Lite", + "OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额度。", + "workspace.lite.promo.modelsTitle": "包含模型", + "workspace.lite.promo.footer": + "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。", + "workspace.lite.promo.subscribe": "订阅 Go", "workspace.lite.promo.subscribing": "正在重定向...", "download.title": "OpenCode | 下载", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 87fcaa8e89e6..8ac69596cb7a 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -334,8 +334,8 @@ export const dict = { "workspace.usage.breakdown.cacheWrite": "快取寫入", "workspace.usage.breakdown.output": "輸出", "workspace.usage.breakdown.reasoning": "推理", - "workspace.usage.subscription": "訂閱 (${{amount}})", - "workspace.usage.lite": "lite (${{amount}})", + "workspace.usage.subscription": "Black (${{amount}})", + "workspace.usage.lite": "Go (${{amount}})", "workspace.usage.byok": "BYOK (${{amount}})", "workspace.cost.title": "成本", @@ -481,20 +481,25 @@ export const dict = { "workspace.lite.time.minute": "分鐘", "workspace.lite.time.minutes": "分鐘", "workspace.lite.time.fewSeconds": "幾秒", - "workspace.lite.subscription.title": "Lite 訂閱", - "workspace.lite.subscription.message": "您已訂閱 OpenCode Lite。", + "workspace.lite.subscription.title": "Go 訂閱", + "workspace.lite.subscription.message": "您已訂閱 OpenCode Go。", "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.other.title": "Lite 訂閱", - "workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Lite。每個工作區只能有一位成員訂閱。", - "workspace.lite.promo.title": "OpenCode Lite", + "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.promo.description": - "每月只需 $10 即可使用最佳的開放模型 — Kimi K2.5、GLM-5 和 MiniMax M2.5 — 並享有慷慨的使用限制。", - "workspace.lite.promo.subscribe": "訂閱 Lite", + "OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。", + "workspace.lite.promo.modelsTitle": "包含模型", + "workspace.lite.promo.footer": + "該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。", + "workspace.lite.promo.subscribe": "訂閱 Go", "workspace.lite.promo.subscribing": "重新導向中...", "download.title": "OpenCode | 下載", diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index 382832e8faf2..8bce3cd464f7 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,16 +1,21 @@ -import { A, useSearchParams } from "@solidjs/router" +import { A, createAsync, query, useSearchParams } from "@solidjs/router" import { Title } from "@solidjs/meta" import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js" import { PlanIcon, plans } from "./common" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" +import { Resource } from "@opencode-ai/console-resource" -const paused = true +const getPaused = query(async () => { + "use server" + return Resource.App.stage === "production" +}, "black.paused") export default function Black() { const [params] = useSearchParams() const i18n = useI18n() const language = useLanguage() + const paused = createAsync(() => getPaused()) const [selected, setSelected] = createSignal((params.plan as string) || null) const [mounted, setMounted] = createSignal(false) const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) @@ -44,7 +49,7 @@ export default function Black() { <> {i18n.t("black.title")}
- {i18n.t("black.paused")}

}> + {i18n.t("black.paused")}

}>
@@ -108,7 +113,7 @@ export default function Black() { - +

{i18n.t("black.finePrint.beforeTerms")} ·{" "} {i18n.t("black.finePrint.terms")} diff --git a/packages/console/app/src/routes/black/_subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx similarity index 98% rename from packages/console/app/src/routes/black/_subscribe/[plan].tsx rename to packages/console/app/src/routes/black/subscribe/[plan].tsx index 644d87d9b325..19b56eabe678 100644 --- a/packages/console/app/src/routes/black/_subscribe/[plan].tsx +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -17,6 +17,12 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { formError } from "~/lib/form-error" +import { Resource } from "@opencode-ai/console-resource" + +const getEnabled = query(async () => { + "use server" + return Resource.App.stage !== "production" +}, "black.subscribe.enabled") const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!) @@ -269,6 +275,7 @@ export default function BlackSubscribe() { const params = useParams() const i18n = useI18n() const language = useLanguage() + const enabled = createAsync(() => getEnabled()) const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"] const plan = planData.id @@ -359,7 +366,7 @@ export default function BlackSubscribe() { } return ( - <> + {i18n.t("black.subscribe.title")}

@@ -472,6 +479,6 @@ export default function BlackSubscribe() { {i18n.t("black.finePrint.terms")}

- +
) } diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 9fbdad2ef742..e039a09ef8b3 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -21,7 +21,7 @@ export default function () { - + diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css index 20662ab61863..76d9bcfb099c 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.module.css @@ -140,6 +140,22 @@ } } + [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); @@ -147,12 +163,26 @@ } [data-slot="promo-description"] { - font-size: var(--font-size-sm); + font-size: var(--font-size-md); 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); diff --git a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx index c9192fdcf695..395d008e1d53 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/lite-section.tsx @@ -11,6 +11,7 @@ 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" @@ -135,6 +136,7 @@ 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) @@ -181,6 +183,13 @@ export function LiteSection() {
+
+ {i18n.t("workspace.lite.subscription.selectProvider")}{" "} + + {i18n.t("common.learnMore")} + + . +
@@ -252,6 +261,13 @@ export function LiteSection() {

{i18n.t("workspace.lite.promo.title")}

{i18n.t("workspace.lite.promo.description")}

+

{i18n.t("workspace.lite.promo.modelsTitle")}

+
    +
  • Kimi K2.5
  • +
  • GLM-5
  • +
  • MiniMax M2.5
  • +
+

{i18n.t("workspace.lite.promo.footer")}

@@ -950,7 +973,6 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre } PART_MAPPING["tool"] = function ToolPartDisplay(props) { - const data = useData() const i18n = useI18n() const part = props.part as ToolPart if (part.tool === "todowrite" || part.tool === "todoread") return null @@ -959,75 +981,18 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { () => part.tool === "question" && (part.state.status === "pending" || part.state.status === "running"), ) - const permission = createMemo(() => { - const next = data.store.permission?.[props.message.sessionID]?.[0] - if (!next || !next.tool) return undefined - if (next.tool!.callID !== part.callID) return undefined - return next - }) - - const questionRequest = createMemo(() => { - const next = data.store.question?.[props.message.sessionID]?.[0] - if (!next || !next.tool) return undefined - if (next.tool!.callID !== part.callID) return undefined - return next - }) - - const [showPermission, setShowPermission] = createSignal(false) - const [showQuestion, setShowQuestion] = createSignal(false) - - createEffect(() => { - const perm = permission() - if (perm) { - const timeout = setTimeout(() => setShowPermission(true), 50) - onCleanup(() => clearTimeout(timeout)) - } else { - setShowPermission(false) - } - }) - - createEffect(() => { - const question = questionRequest() - if (question) { - const timeout = setTimeout(() => setShowQuestion(true), 50) - onCleanup(() => clearTimeout(timeout)) - } else { - setShowQuestion(false) - } - }) - - const [forceOpen, setForceOpen] = createSignal(false) - createEffect(() => { - if (permission() || questionRequest()) setForceOpen(true) - }) - - const respond = (response: "once" | "always" | "reject") => { - const perm = permission() - if (!perm || !data.respondToPermission) return - data.respondToPermission({ - sessionID: perm.sessionID, - permissionID: perm.id, - response, - }) - } - const emptyInput: Record = {} const emptyMetadata: Record = {} const input = () => part.state?.input ?? emptyInput // @ts-expect-error const partMetadata = () => part.state?.metadata ?? emptyMetadata - const metadata = () => { - const perm = permission() - if (perm?.metadata) return { ...perm.metadata, ...partMetadata() } - return partMetadata() - } const render = ToolRegistry.render(part.tool) ?? GenericTool return ( -
+
{(error) => { @@ -1067,33 +1032,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { component={render} input={input()} tool={part.tool} - metadata={metadata()} + metadata={partMetadata()} // @ts-expect-error output={part.state.output} status={part.state.status} hideDetails={props.hideDetails} - forceOpen={forceOpen()} - locked={showPermission() || showQuestion()} defaultOpen={props.defaultOpen} /> - -
-
- - - -
-
-
- {(request) => }
) @@ -1525,6 +1472,7 @@ ToolRegistry.register({ ToolRegistry.register({ name: "edit", render(props) { + const data = useData() const i18n = useI18n() const diffComponent = useDiffComponent() const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) @@ -1567,6 +1515,7 @@ ToolRegistry.register({ openProjectFile(path(), data.directory, data.openFilePath)} actions={ {(diff) => } } @@ -1596,6 +1545,7 @@ ToolRegistry.register({ ToolRegistry.register({ name: "write", render(props) { + const data = useData() const i18n = useI18n() const codeComponent = useCodeComponent() const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) @@ -1632,7 +1582,10 @@ ToolRegistry.register({ } > - + openProjectFile(path(), data.directory, data.openFilePath)} + >
(props.metadata.files ?? []) as ApplyPatchFile[]) @@ -1744,7 +1698,16 @@ ToolRegistry.register({ {`\u202A${getDirectory(file.relativePath)}\u202C`} - {getFilename(file.relativePath)} + { + event.stopPropagation() + openProjectFile(file.relativePath, data.directory, data.openFilePath) + }} + > + {getFilename(file.relativePath)} +
@@ -1829,6 +1792,7 @@ ToolRegistry.register({ > openProjectFile(file().relativePath, data.directory, data.openFilePath)} actions={ @@ -1963,245 +1927,3 @@ ToolRegistry.register({ ) }, }) - -function QuestionPrompt(props: { request: QuestionRequest }) { - const data = useData() - const i18n = useI18n() - const questions = createMemo(() => props.request.questions) - const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true) - - const [store, setStore] = createStore({ - tab: 0, - answers: [] as QuestionAnswer[], - custom: [] as string[], - editing: false, - }) - - const question = createMemo(() => questions()[store.tab]) - const confirm = createMemo(() => !single() && store.tab === questions().length) - const options = createMemo(() => question()?.options ?? []) - const input = createMemo(() => store.custom[store.tab] ?? "") - const multi = createMemo(() => question()?.multiple === true) - const customPicked = createMemo(() => { - const value = input() - if (!value) return false - return store.answers[store.tab]?.includes(value) ?? false - }) - - function submit() { - const answers = questions().map((_, i) => store.answers[i] ?? []) - data.replyToQuestion?.({ - requestID: props.request.id, - answers, - }) - } - - function reject() { - data.rejectQuestion?.({ - requestID: props.request.id, - }) - } - - function pick(answer: string, custom: boolean = false) { - const answers = [...store.answers] - answers[store.tab] = [answer] - setStore("answers", answers) - if (custom) { - const inputs = [...store.custom] - inputs[store.tab] = answer - setStore("custom", inputs) - } - if (single()) { - data.replyToQuestion?.({ - requestID: props.request.id, - answers: [[answer]], - }) - return - } - setStore("tab", store.tab + 1) - } - - function toggle(answer: string) { - const existing = store.answers[store.tab] ?? [] - const next = [...existing] - const index = next.indexOf(answer) - if (index === -1) next.push(answer) - if (index !== -1) next.splice(index, 1) - const answers = [...store.answers] - answers[store.tab] = next - setStore("answers", answers) - } - - function selectTab(index: number) { - setStore("tab", index) - setStore("editing", false) - } - - function selectOption(optIndex: number) { - if (optIndex === options().length) { - setStore("editing", true) - return - } - const opt = options()[optIndex] - if (!opt) return - if (multi()) { - toggle(opt.label) - return - } - pick(opt.label) - } - - function handleCustomSubmit(e: Event) { - e.preventDefault() - const value = input().trim() - if (!value) { - setStore("editing", false) - return - } - if (multi()) { - const existing = store.answers[store.tab] ?? [] - const next = [...existing] - if (!next.includes(value)) next.push(value) - const answers = [...store.answers] - answers[store.tab] = next - setStore("answers", answers) - setStore("editing", false) - return - } - pick(value, true) - setStore("editing", false) - } - - return ( -
- -
- - {(q, index) => { - const active = () => index() === store.tab - const answered = () => (store.answers[index()]?.length ?? 0) > 0 - return ( - - ) - }} - - -
-
- - -
-
- {question()?.question} - {multi() ? " " + i18n.t("ui.question.multiHint") : ""} -
-
- - {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} - - - -
- setTimeout(() => el.focus(), 0)} - type="text" - data-slot="custom-input" - placeholder={i18n.t("ui.question.custom.placeholder")} - value={input()} - onInput={(e) => { - const inputs = [...store.custom] - inputs[store.tab] = e.currentTarget.value - setStore("custom", inputs) - }} - /> - - -
-
-
-
-
- - -
-
{i18n.t("ui.messagePart.review.title")}
- - {(q, index) => { - const value = () => store.answers[index()]?.join(", ") ?? "" - const answered = () => Boolean(value()) - return ( -
- {q.question} - - {answered() ? value() : i18n.t("ui.question.review.notAnswered")} - -
- ) - }} -
-
-
- -
- - - - - - - - - -
-
- ) -} diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 4836a0864c2a..a9dbea7bc4d0 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -65,6 +65,11 @@ function TabsTrigger(props: ParentProps) { ...(split.classList ?? {}), [split.class ?? ""]: !!split.class, }} + onMouseDown={(e) => { + if (e.button === 1 && split.onMiddleClick) { + e.preventDefault() + } + }} onAuxClick={(e) => { if (e.button === 1 && split.onMiddleClick) { e.preventDefault() diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index 2c44763f5368..5fe5dc8aa906 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -1,14 +1,4 @@ -import type { - Message, - Session, - Part, - FileDiff, - SessionStatus, - PermissionRequest, - QuestionRequest, - QuestionAnswer, - ProviderListResponse, -} from "@opencode-ai/sdk/v2" +import type { Message, Session, Part, FileDiff, SessionStatus, ProviderListResponse } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -24,12 +14,6 @@ type Data = { session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] } - permission?: { - [sessionID: string]: PermissionRequest[] - } - question?: { - [sessionID: string]: QuestionRequest[] - } message: { [sessionID: string]: Message[] } @@ -38,30 +22,20 @@ type Data = { } } -export type PermissionRespondFn = (input: { - sessionID: string - permissionID: string - response: "once" | "always" | "reject" -}) => void - -export type QuestionReplyFn = (input: { requestID: string; answers: QuestionAnswer[] }) => void - -export type QuestionRejectFn = (input: { requestID: string }) => void - export type NavigateToSessionFn = (sessionID: string) => void export type SessionHrefFn = (sessionID: string) => string +export type OpenFilePathFn = (input: { path: string; line?: number }) => void + export const { use: useData, provider: DataProvider } = createSimpleContext({ name: "Data", init: (props: { data: Data directory: string - onPermissionRespond?: PermissionRespondFn - onQuestionReply?: QuestionReplyFn - onQuestionReject?: QuestionRejectFn onNavigateToSession?: NavigateToSessionFn onSessionHref?: SessionHrefFn + onOpenFilePath?: OpenFilePathFn }) => { return { get store() { @@ -70,11 +44,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ get directory() { return props.directory }, - respondToPermission: props.onPermissionRespond, - replyToQuestion: props.onQuestionReply, - rejectQuestion: props.onQuestionReject, navigateToSession: props.onNavigateToSession, sessionHref: props.onSessionHref, + openFilePath: props.onOpenFilePath, } }, }) diff --git a/packages/util/package.json b/packages/util/package.json index 4bcbb0305d4e..36a235639ee9 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.10", + "version": "1.2.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index b14a7ccb8f8e..612d4fb8cdd9 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -314,7 +314,7 @@ function configSchema() { hooks: { "astro:build:done": async () => { console.log("generating config schema") - spawnSync("../opencode/script/schema.ts", ["./dist/config.json"]) + spawnSync("../opencode/script/schema.ts", ["./dist/config.json", "./dist/tui.json"]) }, }, } diff --git a/packages/web/package.json b/packages/web/package.json index 110c6ca2354f..daf2ad3480f7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.10", + "version": "1.2.15", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/ar/share.mdx b/packages/web/src/content/docs/ar/share.mdx index 535d44dadf83..6d13410458cd 100644 --- a/packages/web/src/content/docs/ar/share.mdx +++ b/packages/web/src/content/docs/ar/share.mdx @@ -41,7 +41,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ description: شارك محادثات OpenCode الخاصة بك. ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/bs/share.mdx b/packages/web/src/content/docs/bs/share.mdx index a15e15074349..b0760ee0c138 100644 --- a/packages/web/src/content/docs/bs/share.mdx +++ b/packages/web/src/content/docs/bs/share.mdx @@ -41,7 +41,7 @@ Da eksplicitno postavite rucni nacin u [config datoteci](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Mozete ukljuciti automatsko dijeljenje za sve nove razgovore tako sto `share` po ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Dijeljenje mozete potpuno iskljuciti tako sto `share` postavite na `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index c504f734fa5d..6b1c3dee57e2 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -558,6 +558,7 @@ OpenCode can be configured using environment variables. | `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | | `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | | `OPENCODE_CONFIG` | string | Path to config file | +| `OPENCODE_TUI_CONFIG` | string | Path to TUI config file | | `OPENCODE_CONFIG_DIR` | string | Path to config directory | | `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | | `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index eeccde2f7913..038f253274e9 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -14,10 +14,11 @@ OpenCode supports both **JSON** and **JSONC** (JSON with Comments) formats. ```jsonc title="opencode.jsonc" { "$schema": "https://opencode.ai/config.json", - // Theme configuration - "theme": "opencode", "model": "anthropic/claude-sonnet-4-5", "autoupdate": true, + "server": { + "port": 4096, + }, } ``` @@ -34,7 +35,7 @@ Configuration files are **merged together**, not replaced. Configuration files are merged together, not replaced. Settings from the following config locations are combined. Later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved. -For example, if your global config sets `theme: "opencode"` and `autoupdate: true`, and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include all three settings. +For example, if your global config sets `autoupdate: true` and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include both settings. --- @@ -95,7 +96,9 @@ You can enable specific servers in your local config: ### Global -Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide preferences like themes, providers, or keybinds. +Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide server/runtime preferences like providers, models, and permissions. + +For TUI-specific settings, use `~/.config/opencode/tui.json`. Global config overrides remote organizational defaults. @@ -105,6 +108,8 @@ Global config overrides remote organizational defaults. Add `opencode.json` in your project root. Project config has the highest precedence among standard config files - it overrides both global and remote configs. +For project-specific TUI settings, add `tui.json` alongside it. + :::tip Place project specific config in the root of your project. ::: @@ -146,7 +151,9 @@ The custom directory is loaded after the global config and `.opencode` directori ## Schema -The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json). +The server/runtime config schema is defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json). + +TUI config uses [**`opencode.ai/tui.json`**](https://opencode.ai/tui.json). Your editor should be able to validate and autocomplete based on the schema. @@ -154,28 +161,24 @@ Your editor should be able to validate and autocomplete based on the schema. ### TUI -You can configure TUI-specific settings through the `tui` option. +Use a dedicated `tui.json` (or `tui.jsonc`) file for TUI-specific settings. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "tui": { - "scroll_speed": 3, - "scroll_acceleration": { - "enabled": true - }, - "diff_style": "auto" - } + "$schema": "https://opencode.ai/tui.json", + "scroll_speed": 3, + "scroll_acceleration": { + "enabled": true + }, + "diff_style": "auto" } ``` -Available options: +Use `OPENCODE_TUI_CONFIG` to point to a custom TUI config file. -- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.** -- `scroll_speed` - Custom scroll speed multiplier (default: `3`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. -- `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column. +Legacy `theme`, `keybinds`, and `tui` keys in `opencode.json` are deprecated and automatically migrated when possible. -[Learn more about using the TUI here](/docs/tui). +[Learn more about TUI configuration here](/docs/tui#configure). --- @@ -301,12 +304,12 @@ Bearer tokens (`AWS_BEARER_TOKEN_BEDROCK` or `/connect`) take precedence over pr ### Themes -You can configure the theme you want to use in your OpenCode config through the `theme` option. +Set your UI theme in `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "theme": "" + "$schema": "https://opencode.ai/tui.json", + "theme": "tokyonight" } ``` @@ -406,11 +409,11 @@ You can also define commands using markdown files in `~/.config/opencode/command ### Keybinds -You can customize your keybinds through the `keybinds` option. +Customize keybinds in `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": {} } ``` diff --git a/packages/web/src/content/docs/da/share.mdx b/packages/web/src/content/docs/da/share.mdx index 1ac2094ca700..80b9f0959e2c 100644 --- a/packages/web/src/content/docs/da/share.mdx +++ b/packages/web/src/content/docs/da/share.mdx @@ -41,7 +41,7 @@ For at eksplisitt angi manuell modus i [konfigurasjonsfilen](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Du kan aktivere automatisk deling for alle nye samtaler ved at sette alternative ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Du kan deaktivere deling helt ved at sette alternativet `share` til `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/de/share.mdx b/packages/web/src/content/docs/de/share.mdx index 99fad21099b0..9b7e9284c7ac 100644 --- a/packages/web/src/content/docs/de/share.mdx +++ b/packages/web/src/content/docs/de/share.mdx @@ -43,7 +43,7 @@ Um den manuellen Modus explizit in der [Konfiguration](/docs/config) zu setzen: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -56,7 +56,7 @@ Du kannst automatisches Teilen fuer neue Unterhaltungen aktivieren, indem du in ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -71,7 +71,7 @@ Du kannst Teilen komplett deaktivieren, indem du in der [Konfiguration](/docs/co ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/es/share.mdx b/packages/web/src/content/docs/es/share.mdx index e1c62a031c04..3bb376f38625 100644 --- a/packages/web/src/content/docs/es/share.mdx +++ b/packages/web/src/content/docs/es/share.mdx @@ -41,7 +41,7 @@ Para configurar explícitamente el modo manual en su [archivo de configuración] ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Puede habilitar el uso compartido automático para todas las conversaciones nuev ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Puede desactivar el uso compartido por completo configurando la opción `share` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/fr/share.mdx b/packages/web/src/content/docs/fr/share.mdx index acc7c03f83e9..e6b067a8c82a 100644 --- a/packages/web/src/content/docs/fr/share.mdx +++ b/packages/web/src/content/docs/fr/share.mdx @@ -41,7 +41,7 @@ Pour définir explicitement le mode manuel dans votre [fichier de configuration] ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Vous pouvez activer le partage automatique pour toutes les nouvelles conversatio ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Vous pouvez désactiver entièrement le partage en définissant l'option `share` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/it/share.mdx b/packages/web/src/content/docs/it/share.mdx index f9eff6ca9bd5..9b410a6b8654 100644 --- a/packages/web/src/content/docs/it/share.mdx +++ b/packages/web/src/content/docs/it/share.mdx @@ -41,7 +41,7 @@ Per impostare esplicitamente la modalita manuale nel tuo [file di config](/docs/ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Puoi abilitare la condivisione automatica per tutte le nuove conversazioni impos ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Puoi disabilitare completamente la condivisione impostando l'opzione `share` su ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/ja/share.mdx b/packages/web/src/content/docs/ja/share.mdx index 7995ba9a075e..606e807dc8aa 100644 --- a/packages/web/src/content/docs/ja/share.mdx +++ b/packages/web/src/content/docs/ja/share.mdx @@ -41,7 +41,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode は、会話の共有方法を制御する 3 つの共有モードを ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx index 25fe2a1d9100..95b3d496391d 100644 --- a/packages/web/src/content/docs/keybinds.mdx +++ b/packages/web/src/content/docs/keybinds.mdx @@ -3,11 +3,11 @@ title: Keybinds description: Customize your keybinds. --- -OpenCode has a list of keybinds that you can customize through the OpenCode config. +OpenCode has a list of keybinds that you can customize through `tui.json`. -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": { "leader": "ctrl+x", "app_exit": "ctrl+c,ctrl+d,q", @@ -117,11 +117,11 @@ You don't need to use a leader key for your keybinds but we recommend doing so. ## Disable keybind -You can disable a keybind by adding the key to your config with a value of "none". +You can disable a keybind by adding the key to `tui.json` with a value of "none". -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "keybinds": { "session_compact": "none" } diff --git a/packages/web/src/content/docs/ko/share.mdx b/packages/web/src/content/docs/ko/share.mdx index 55cf6a2c3e39..9e5c6388243e 100644 --- a/packages/web/src/content/docs/ko/share.mdx +++ b/packages/web/src/content/docs/ko/share.mdx @@ -41,7 +41,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ opencode는 대화가 공유되는 방법을 제어하는 세 가지 공유 모 ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/nb/share.mdx b/packages/web/src/content/docs/nb/share.mdx index 370477d1cfc5..ca0cb4829acb 100644 --- a/packages/web/src/content/docs/nb/share.mdx +++ b/packages/web/src/content/docs/nb/share.mdx @@ -41,7 +41,7 @@ For å eksplisitt angi manuell modus i [konfigurasjonsfilen](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Du kan aktivere automatisk deling for alle nye samtaler ved å sette alternative ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Du kan deaktivere deling helt ved å sette alternativet `share` til `"disabled"` ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/pl/share.mdx b/packages/web/src/content/docs/pl/share.mdx index 463019295a3f..0389267b59ad 100644 --- a/packages/web/src/content/docs/pl/share.mdx +++ b/packages/web/src/content/docs/pl/share.mdx @@ -41,7 +41,7 @@ Aby jawnie ustawić tryb ręczny w [pliku konfiguracyjnym] (./config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Możesz włączyć automatyczne udostępnianie dla wszystkich nowych rozmów, us ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Możesz całkowicie wyłączyć udostępnianie, ustawiając opcję `share` na `" ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index db3bfeaeebeb..34e3626499cb 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -57,7 +57,39 @@ tested and verified to work well with OpenCode. [Learn more](/docs/zen). If you are new, we recommend starting with OpenCode Zen. ::: -1. Run the `/connect` command in the TUI, select opencode, and head to [opencode.ai/auth](https://opencode.ai/auth). +1. Run the `/connect` command in the TUI, select `OpenCode Zen`, and head to [opencode.ai/auth](https://opencode.ai/zen). + + ```txt + /connect + ``` + +2. Sign in, add your billing details, and copy your API key. + +3. Paste your API key. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +4. Run `/models` in the TUI to see the list of models we recommend. + + ```txt + /models + ``` + +It works like any other provider in OpenCode and is completely optional to use. + +--- + +## OpenCode Go + +OpenCode Go is a low cost subscription plan that provides reliable access to popular open coding models provided by the OpenCode team that have been +tested and verified to work well with OpenCode. + +1. Run the `/connect` command in the TUI, select `OpenCode Go`, and head to [opencode.ai/auth](https://opencode.ai/zen). ```txt /connect diff --git a/packages/web/src/content/docs/pt-br/share.mdx b/packages/web/src/content/docs/pt-br/share.mdx index 5aa0439d068b..166226d6dc28 100644 --- a/packages/web/src/content/docs/pt-br/share.mdx +++ b/packages/web/src/content/docs/pt-br/share.mdx @@ -41,7 +41,7 @@ Para definir explicitamente o modo manual em seu [arquivo de configuração](/do ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Você pode habilitar o compartilhamento automático para todas as novas conversa ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Você pode desativar o compartilhamento completamente definindo a opção `share ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/ru/share.mdx b/packages/web/src/content/docs/ru/share.mdx index c4df3b6a7032..8982afb08dfb 100644 --- a/packages/web/src/content/docs/ru/share.mdx +++ b/packages/web/src/content/docs/ru/share.mdx @@ -41,7 +41,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ opencode поддерживает три режима общего доступ ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/share.mdx b/packages/web/src/content/docs/share.mdx index 475ee08d041c..b2c79334097d 100644 --- a/packages/web/src/content/docs/share.mdx +++ b/packages/web/src/content/docs/share.mdx @@ -41,7 +41,7 @@ To explicitly set manual mode in your [config file](/docs/config): ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ You can enable automatic sharing for all new conversations by setting the `share ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ You can disable sharing entirely by setting the `share` option to `"disabled"` i ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/th/share.mdx b/packages/web/src/content/docs/th/share.mdx index 195d7696f9b1..91bfce44172c 100644 --- a/packages/web/src/content/docs/th/share.mdx +++ b/packages/web/src/content/docs/th/share.mdx @@ -41,7 +41,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode รองรับโหมดการแชร์สามโหม ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/themes.mdx b/packages/web/src/content/docs/themes.mdx index d37ce3135569..8a7c6a46ac8a 100644 --- a/packages/web/src/content/docs/themes.mdx +++ b/packages/web/src/content/docs/themes.mdx @@ -61,11 +61,11 @@ The system theme is for users who: ## Using a theme -You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in your [config](/docs/config). +You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in `tui.json`. -```json title="opencode.json" {3} +```json title="tui.json" {3} { - "$schema": "https://opencode.ai/config.json", + "$schema": "https://opencode.ai/tui.json", "theme": "tokyonight" } ``` diff --git a/packages/web/src/content/docs/tr/share.mdx b/packages/web/src/content/docs/tr/share.mdx index 1b7abfdb7ea0..a0544eb02aa8 100644 --- a/packages/web/src/content/docs/tr/share.mdx +++ b/packages/web/src/content/docs/tr/share.mdx @@ -41,7 +41,7 @@ Manuel modu acikca ayarlamak icin [config dosyaniza](/docs/config) sunu ekleyin: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ Tum yeni konusmalar icin otomatik paylasimi acmak isterseniz, [config dosyanizda ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ Paylasimi tamamen kapatmak icin [config dosyanizda](/docs/config) `share` degeri ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index 1e48d42ccb1e..010e8328f419 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -355,24 +355,34 @@ Some editors need command-line arguments to run in blocking mode. The `--wait` f ## Configure -You can customize TUI behavior through your OpenCode config file. +You can customize TUI behavior through `tui.json` (or `tui.jsonc`). -```json title="opencode.json" +```json title="tui.json" { - "$schema": "https://opencode.ai/config.json", - "tui": { - "scroll_speed": 3, - "scroll_acceleration": { - "enabled": true - } - } + "$schema": "https://opencode.ai/tui.json", + "theme": "opencode", + "keybinds": { + "leader": "ctrl+x" + }, + "scroll_speed": 3, + "scroll_acceleration": { + "enabled": true + }, + "diff_style": "auto" } ``` +This is separate from `opencode.json`, which configures server/runtime behavior. + ### Options -- `scroll_acceleration` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** -- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `theme` - Sets your UI theme. [Learn more](/docs/themes). +- `keybinds` - Customizes keyboard shortcuts. [Learn more](/docs/keybinds). +- `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** +- `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `0.001`, supports decimal values). Defaults to `3`. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `diff_style` - Controls diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows a single-column layout. + +Use `OPENCODE_TUI_CONFIG` to load a custom TUI config path. --- diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 453093206b98..48c040cf2dff 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -64,6 +64,7 @@ You can also access our models through the following API endpoints. | Model | Model ID | Endpoint | AI SDK Package | | ------------------ | ------------------ | -------------------------------------------------- | --------------------------- | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -88,11 +89,9 @@ You can also access our models through the following API endpoints. | MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 Free | glm-5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -124,11 +123,9 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | MiniMax M2.5 Free | Free | Free | Free | - | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | | MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | -| GLM 5 Free | Free | Free | Free | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | GLM 4.7 | $0.60 | $2.20 | $0.10 | - | | GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 Free | Free | Free | Free | - | | Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | | Kimi K2 Thinking | $0.40 | $2.50 | - | - | | Kimi K2 | $0.40 | $2.50 | - | - | @@ -150,6 +147,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | +| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - | | GPT 5.2 | $1.75 | $14.00 | $0.175 | - | | GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | | GPT 5.1 | $1.07 | $8.50 | $0.107 | - | @@ -168,8 +166,6 @@ Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don The free models: -- GLM 5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. -- Kimi K2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - MiniMax M2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. @@ -201,8 +197,6 @@ charging you more than $20 if your balance goes below $5. All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions: - Big Pickle: During its free period, collected data may be used to improve the model. -- GLM 5 Free: During its free period, collected data may be used to improve the model. -- Kimi K2.5 Free: During its free period, collected data may be used to improve the model. - MiniMax M2.5 Free: During its free period, collected data may be used to improve the model. - OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). - Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage). diff --git a/packages/web/src/content/docs/zh-cn/custom-tools.mdx b/packages/web/src/content/docs/zh-cn/custom-tools.mdx index 81a90a2bcb57..8b44a0450c2a 100644 --- a/packages/web/src/content/docs/zh-cn/custom-tools.mdx +++ b/packages/web/src/content/docs/zh-cn/custom-tools.mdx @@ -79,6 +79,32 @@ export const multiply = tool({ --- +#### 与内置工具的名称冲突 + +自定义工具通过工具名称进行索引。如果自定义工具使用了与内置工具相同的名称,则优先使用自定义工具。 + +例如,这个文件取代了内置的bash工具: + +```ts title=".opencode/tools/bash.ts" +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Restricted bash wrapper", + args: { + command: tool.schema.string(), + }, + async execute(args) { + return `blocked: ${args.command}` + }, +}) +``` + +:::note +除非你有意替换内置工具,否则最好用独特的名字。如果你想禁用内置工具但不想覆盖它,使用 [权限](/docs/permissions). +::: + +--- + ### 参数 你可以使用 `tool.schema`(即 [Zod](https://zod.dev))来定义参数类型。 diff --git a/packages/web/src/content/docs/zh-cn/lsp.mdx b/packages/web/src/content/docs/zh-cn/lsp.mdx index 57b812190215..59dd7082a1e0 100644 --- a/packages/web/src/content/docs/zh-cn/lsp.mdx +++ b/packages/web/src/content/docs/zh-cn/lsp.mdx @@ -27,6 +27,7 @@ OpenCode 内置了多种适用于主流语言的 LSP 服务器: | gopls | .go | 需要 `go` 命令可用 | | hls | .hs, .lhs | 需要 `haskell-language-server-wrapper` 命令可用 | | jdtls | .java | 需要已安装 `Java SDK (version 21+)` | +| julials | .jl | 需要安装 `julia` and `LanguageServer.jl` | | kotlin-ls | .kt, .kts | 为 Kotlin 项目自动安装 | | lua-ls | .lua | 为 Lua 项目自动安装 | | nixd | .nix | 需要 `nixd` 命令可用 | diff --git a/packages/web/src/content/docs/zh-cn/plugins.mdx b/packages/web/src/content/docs/zh-cn/plugins.mdx index 0df6d1ee6591..e8a8bd70cbc3 100644 --- a/packages/web/src/content/docs/zh-cn/plugins.mdx +++ b/packages/web/src/content/docs/zh-cn/plugins.mdx @@ -307,6 +307,10 @@ export const CustomToolsPlugin: Plugin = async (ctx) => { 你的自定义工具将与内置工具一起在 OpenCode 中可用。 +:::note +如果插件工具与内置工具使用相同的名称,则优先使用插件工具。 +::: + --- ### 日志记录 diff --git a/packages/web/src/content/docs/zh-cn/providers.mdx b/packages/web/src/content/docs/zh-cn/providers.mdx index ccc2bf7d406b..9c1616876d72 100644 --- a/packages/web/src/content/docs/zh-cn/providers.mdx +++ b/packages/web/src/content/docs/zh-cn/providers.mdx @@ -131,6 +131,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 2. 使用以下方法之一**配置身份验证**: + *** + #### 环境变量(快速上手) 运行 opencode 时设置以下环境变量之一: @@ -153,6 +155,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 export AWS_REGION=us-east-1 ``` + *** + #### 配置文件(推荐) 如需项目级别或持久化的配置,请使用 `opencode.json`: @@ -180,6 +184,8 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 配置文件中的选项优先级高于环境变量。 ::: + *** + #### 进阶:VPC 端点 如果你使用 Bedrock 的 VPC 端点: @@ -203,12 +209,16 @@ OpenCode Zen 是由 OpenCode 团队提供的模型列表,这些模型已经过 `endpoint` 选项是通用 `baseURL` 选项的别名,使用了 AWS 特有的术语。如果同时指定了 `endpoint` 和 `baseURL`,则 `endpoint` 优先。 ::: + *** + #### 认证方式 - **`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`**:在 AWS 控制台中创建 IAM 用户并生成访问密钥 - **`AWS_PROFILE`**:使用 `~/.aws/credentials` 中的命名配置文件。需要先通过 `aws configure --profile my-profile` 或 `aws sso login` 进行配置 - **`AWS_BEARER_TOKEN_BEDROCK`**:从 Amazon Bedrock 控制台生成长期 API 密钥 - **`AWS_WEB_IDENTITY_TOKEN_FILE` / `AWS_ROLE_ARN`**:适用于 EKS IRSA(服务账户的 IAM 角色)或其他支持 OIDC 联合的 Kubernetes 环境。使用服务账户注解时,Kubernetes 会自动注入这些环境变量。 + *** + #### 认证优先级 Amazon Bedrock 使用以下认证优先级: diff --git a/packages/web/src/content/docs/zh-cn/share.mdx b/packages/web/src/content/docs/zh-cn/share.mdx index 8a7be16dc91f..a2b34688e4dc 100644 --- a/packages/web/src/content/docs/zh-cn/share.mdx +++ b/packages/web/src/content/docs/zh-cn/share.mdx @@ -41,7 +41,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode 支持三种分享模式,用于控制对话的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/packages/web/src/content/docs/zh-cn/tui.mdx b/packages/web/src/content/docs/zh-cn/tui.mdx index e34c088cb3a9..df8ce38fecc3 100644 --- a/packages/web/src/content/docs/zh-cn/tui.mdx +++ b/packages/web/src/content/docs/zh-cn/tui.mdx @@ -234,7 +234,7 @@ How is auth handled in @packages/functions/src/api/index.ts? 列出可用主题。 ```bash frame="none" -/theme +/themes ``` **快捷键:** `ctrl+x t` diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 39358c417007..e3fe35e8672b 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -64,19 +64,22 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 Free | glm-5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -104,42 +107,47 @@ https://opencode.ai/zen/v1/models 我们支持按量付费模式。以下是**每 100 万 Token** 的价格。 -| 模型 | 输入 | 输出 | 缓存读取 | 缓存写入 | -| -------------------------------- | ------ | ------ | -------- | -------- | -| Big Pickle | 免费 | 免费 | 免费 | - | -| MiniMax M2.5 Free | 免费 | 免费 | 免费 | - | -| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | -| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | -| GLM 5 | $1.00 | $3.20 | $0.20 | - | -| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | -| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | -| Kimi K2.5 Free | 免费 | 免费 | 免费 | - | -| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | -| Kimi K2 Thinking | $0.40 | $2.50 | - | - | -| Kimi K2 | $0.40 | $2.50 | - | - | -| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | -| Claude Sonnet 4.5 (≤ 200K Token) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4.5 (> 200K Token) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Sonnet 4 (≤ 200K Token) | $3.00 | $15.00 | $0.30 | $3.75 | -| Claude Sonnet 4 (> 200K Token) | $6.00 | $22.50 | $0.60 | $7.50 | -| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | -| Claude Opus 4.6 (≤ 200K Token) | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.6 (> 200K Token) | $10.00 | $37.50 | $1.00 | $12.50 | -| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | -| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | -| Gemini 3 Pro (≤ 200K Token) | $2.00 | $12.00 | $0.20 | - | -| Gemini 3 Pro (> 200K Token) | $4.00 | $18.00 | $0.40 | - | -| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | -| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | -| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | -| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | -| GPT 5 | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | 免费 | 免费 | 免费 | - | +| 模型 | 输入 | 输出 | 缓存读取 | 缓存写入 | +| --------------------------------- | ------ | ------ | -------- | -------- | +| Big Pickle | 免费 | 免费 | 免费 | - | +| MiniMax M2.5 Free | 免费 | 免费 | 免费 | - | +| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - | +| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - | +| GLM 5 Free | Free | Free | Free | - | +| GLM 5 | $1.00 | $3.20 | $0.20 | - | +| GLM 4.7 | $0.60 | $2.20 | $0.10 | - | +| GLM 4.6 | $0.60 | $2.20 | $0.10 | - | +| Kimi K2.5 Free | 免费 | 免费 | 免费 | - | +| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - | +| Kimi K2 Thinking | $0.40 | $2.50 | - | - | +| Kimi K2 | $0.40 | $2.50 | - | - | +| Qwen3 Coder 480B | $0.45 | $1.50 | - | - | +| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 | +| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | +| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 | +| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | +| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | +| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | +| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | +| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | +| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | +| GPT 5.2 | $1.75 | $14.00 | $0.175 | - | +| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - | +| GPT 5.1 | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - | +| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | +| GPT 5 | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | +| GPT 5 Nano | 免费 | 免费 | 免费 | - | 你可能会在使用记录中看到 _Claude Haiku 3.5_。这是一个[低成本模型](/docs/config/#models),用于生成会话标题。 @@ -149,6 +157,7 @@ https://opencode.ai/zen/v1/models 免费模型说明: +- GLM 5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Kimi K2.5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - MiniMax M2.5 Free 在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Big Pickle 是一个隐身模型,在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 @@ -178,6 +187,7 @@ https://opencode.ai/zen/v1/models 我们所有的模型都托管在美国。我们的提供商遵循零保留政策,不会将你的数据用于模型训练,但以下情况除外: - Big Pickle:在免费期间,收集的数据可能会被用于改进模型。 +- GLM 5 Free:在免费期间,收集的数据可能会被用于改进模型。 - Kimi K2.5 Free:在免费期间,收集的数据可能会被用于改进模型。 - MiniMax M2.5 Free:在免费期间,收集的数据可能会被用于改进模型。 - OpenAI API:请求会根据 [OpenAI 数据政策](https://platform.openai.com/docs/guides/your-data)保留 30 天。 diff --git a/packages/web/src/content/docs/zh-tw/share.mdx b/packages/web/src/content/docs/zh-tw/share.mdx index 1512007bc356..58365035b640 100644 --- a/packages/web/src/content/docs/zh-tw/share.mdx +++ b/packages/web/src/content/docs/zh-tw/share.mdx @@ -41,7 +41,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "manual" } ``` @@ -54,7 +54,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "auto" } ``` @@ -69,7 +69,7 @@ OpenCode 支援三種分享模式,用於控制對話的共享方式: ```json title="opencode.json" { - "$schema": "https://opncd.ai/config.json", + "$schema": "https://opencode.ai/config.json", "share": "disabled" } ``` diff --git a/script/beta.ts b/script/beta.ts index a5fb027e6330..b0e6c2dcc15f 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -30,6 +30,52 @@ Please resolve this issue to include this PR in the next beta release.` } } +async function conflicts() { + const out = await $`git diff --name-only --diff-filter=U`.text().catch(() => "") + return out + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) +} + +async function cleanup() { + try { + await $`git merge --abort` + } catch {} + try { + await $`git checkout -- .` + } catch {} + try { + await $`git clean -fd` + } catch {} +} + +async function fix(pr: PR, files: string[]) { + console.log(` Trying to auto-resolve ${files.length} conflict(s) with opencode...`) + const prompt = [ + `Resolve the current git merge conflicts while merging PR #${pr.number} into the beta branch.`, + `Only touch these files: ${files.join(", ")}.`, + "Keep the merge in progress, do not abort the merge, and do not create a commit.", + "When done, leave the working tree with no unmerged files.", + ].join("\n") + + try { + await $`opencode run -m opencode/gpt-5.3-codex ${prompt}` + } catch (err) { + console.log(` opencode failed: ${err}`) + return false + } + + const left = await conflicts() + if (left.length > 0) { + console.log(` Conflicts remain: ${left.join(", ")}`) + return false + } + + console.log(" Conflicts resolved with opencode") + return true +} + async function main() { console.log("Fetching open PRs with beta label...") @@ -69,19 +115,22 @@ async function main() { try { await $`git merge --no-commit --no-ff pr/${pr.number}` } catch { - console.log(" Failed to merge (conflicts)") - try { - await $`git merge --abort` - } catch {} - try { - await $`git checkout -- .` - } catch {} - try { - await $`git clean -fd` - } catch {} - failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) - await commentOnPR(pr.number, "Merge conflicts with dev branch") - continue + const files = await conflicts() + if (files.length > 0) { + console.log(" Failed to merge (conflicts)") + if (!(await fix(pr, files))) { + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) + await commentOnPR(pr.number, "Merge conflicts with dev branch") + continue + } + } else { + console.log(" Failed to merge") + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge failed" }) + await commentOnPR(pr.number, "Merge failed") + continue + } } try { diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 2e2807923eab..a041b65223dd 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.10", + "version": "1.2.15", "publisher": "sst-dev", "repository": { "type": "git",