Compare commits

...

61 Commits

Author SHA1 Message Date
opencode-agent[bot]
d8fbe0af01 chore: update nix node_modules hashes 2026-03-12 08:26:49 +00:00
Brendan Allan
b76ead3fe8 refactor(desktop): rework default server initialization and connection handling (#16965) 2026-03-12 08:10:52 +00:00
opencode-agent[bot]
51835ecf90 chore: generate 2026-03-12 07:36:35 +00:00
Luke Parker
328c6de80d Fix terminal e2e flakiness with a real terminal driver (#17144) 2026-03-12 17:35:26 +10:00
OpeOginni
c9c0318e0e fix(desktop): set default WebSocket username and prevent repeated calling of terminal spawn properly closing the terminal (#17061) 2026-03-12 14:48:44 +08:00
Luke Parker
d481f64bde fix(electron): theme Windows titlebar overlay (#16843)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-03-12 16:38:56 +10:00
Luke Parker
54e7baa6cf fix(desktop-electron): fix resource loading under file:// protocol (#17125) 2026-03-12 12:19:44 +08:00
opencode-agent[bot]
1d7fcd40b4 chore: generate 2026-03-12 03:56:04 +00:00
Luke Parker
db7bafe917 fix(app): guard comment accessor in message timeline (#17126) 2026-03-12 13:55:16 +10:00
Dax Raad
b1ef501207 Merge remote-tracking branch 'origin/dev' into dev 2026-03-11 23:24:38 -04:00
Dax Raad
9fb12a906e core: remove external sourcemap generation to reduce build artifacts 2026-03-11 23:24:26 -04:00
Luke Parker
fafbc29316 fix(ci): use dynamic bun cache path for cross-platform support (#17120) 2026-03-12 03:19:28 +00:00
opencode-agent[bot]
7b0def4b81 chore: generate 2026-03-12 02:04:26 +00:00
Luke Parker
1d9c83b576 fix(e2e): re-focus prompt after terminal opens in slash-terminal test (#17113) 2026-03-12 12:03:38 +10:00
opencode-agent[bot]
2c825c3223 chore: generate 2026-03-12 01:50:44 +00:00
Kit Langton
2a4dedc210 feat(id): brand PermissionID, PtyID, QuestionID, and ToolID (#17042) 2026-03-12 01:49:57 +00:00
opencode-agent[bot]
b0bca6342e chore: generate 2026-03-12 00:26:05 +00:00
Luke Parker
547eb7676d feat(windows): add arm64 release targets for cli and desktop (#16696) 2026-03-12 00:25:09 +00:00
opencode-agent[bot]
83f083ee0d chore: generate 2026-03-11 23:41:43 +00:00
Kit Langton
090f636354 feat(id): brand PartID through Drizzle and Zod schemas (#16966) 2026-03-11 23:40:50 +00:00
opencode-agent[bot]
d26c6f80e1 chore: generate 2026-03-11 23:31:07 +00:00
Kit Langton
16a6d6feba feat(id): brand WorkspaceID through Drizzle and Zod schemas (#16964) 2026-03-11 23:30:17 +00:00
John Mylchreest
f1c3a44190 fix: resolve symlinks in Instance cache to prevent duplicate contexts (#16651)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-03-11 23:26:54 +00:00
opencode-agent[bot]
34fa5de9c5 chore: generate 2026-03-11 23:17:42 +00:00
Kit Langton
cb67465675 feat(id): brand SessionID through Drizzle and Zod schemas (#16953) 2026-03-11 23:16:56 +00:00
Frank
4e73473119 wip: zen 2026-03-11 19:00:05 -04:00
Frank
cc18fa599c wip: zen 2026-03-11 18:50:49 -04:00
Frank
aa81c1c4cb docs: go pricing 2026-03-11 18:09:41 -04:00
Frank
8569fc1f0e docs: zen update models 2026-03-11 18:09:41 -04:00
Frank
78de287bcc wip: zen 2026-03-11 18:09:41 -04:00
Frank
bbc7052c7a go: dashboard design 2026-03-11 18:09:41 -04:00
Frank
502d6db6d0 go: first month discount 2026-03-11 18:09:41 -04:00
Frank
0b0ad5de99 zen: update discount copy on lander 2026-03-11 18:09:41 -04:00
Frank
9e6c4a01aa zen: add alipay for adding balance 2026-03-11 18:09:41 -04:00
Frank
4a81df190c zen: add alipay for go sub 2026-03-11 18:09:41 -04:00
Frank
75cae81f75 zen: add Go page 2026-03-11 18:09:41 -04:00
Frank
ed3bb3ea8f zen: add usage section 2026-03-11 18:09:40 -04:00
Frank
fac23a1afc zen: update usage graph on landing page 2026-03-11 18:09:40 -04:00
Frank
f89696509e zen: update header 2026-03-11 18:09:40 -04:00
Dax Raad
604ab1bde1 core: restore plugin serverUrl getter so plugins can connect to local server 2026-03-11 17:41:51 -04:00
Adam
fbd9b7cf4f feat(app): restore to message and fork session (#17092) 2026-03-11 21:34:48 +00:00
Adam
58f45ae22b chore: skip test 2026-03-11 16:21:04 -05:00
Noam Bressler
440405dbdd fix: re-enable snapshot in acp (#14918) 2026-03-11 16:18:40 -05:00
Adam
a1cda29012 chore: fix test 2026-03-11 16:11:02 -05:00
Aiden Cline
f96e2d4222 tweak: adjust skill presentation to be a little less token heavy (#17098) 2026-03-11 16:03:15 -05:00
Adam
387ab78bf6 chore: fix test 2026-03-11 16:02:11 -05:00
Kit Langton
dbc00aa8e0 feat(id): brand ProjectID through Drizzle and Zod schemas (#16948) 2026-03-11 16:44:26 -04:00
Adam
c37f7b9d99 fix(app): todos not clearing 2026-03-11 14:42:34 -05:00
Chris Yang
cf7ca9b2f7 fix(app): skip editor reconcile during IME composition (#17041) 2026-03-11 13:40:06 -05:00
Kit Langton
981c7b9e37 refactor(account): tighten effect-based account flows (#17072) 2026-03-11 18:18:58 +00:00
Johannes Loher
2aae0d3493 fix(core): read stdout and stderr in PackageRegistry.info before waiting for the process to exit (#16998) 2026-03-11 13:10:45 -05:00
Adam
bcc0d19867 chore(app): simplify review pane (#17066) 2026-03-11 12:24:51 -05:00
xinxin
9c585bb58b docs(providers): clarify npm choice for chat vs responses APIs (#16974)
Co-authored-by: wangxinxin <xinxin.wang@pharmbrain.com>
2026-03-11 10:35:16 -05:00
Aiden Cline
0f6bc8ae71 tweak: adjust way skills are presented to agent to increase likelyhood of skill invocations. (#17053) 2026-03-11 10:24:55 -05:00
Shoubhit Dash
7291e28273 perf(app): trim session render work (#16987) 2026-03-11 18:19:17 +05:30
Filip
db57fe6193 fix(app): make error tool card respect settings (#17005) 2026-03-11 14:52:33 +05:30
Brendan Allan
802416639b ci: setup node in tauri build 2026-03-11 16:09:17 +08:00
opencode-agent[bot]
7ec398d855 chore: generate 2026-03-11 03:34:02 +00:00
Luke Parker
4ab35d2c5c fix(electron): hide Windows background consoles (#16842)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-03-11 13:33:06 +10:00
SOUMITRA-SAHA
b4ae030fc2 fix: add GOOGLE_VERTEX_LOCATION env var support for Vertex AI (#16922)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-10 22:32:39 -05:00
Jack
0843964eb3 feat(web): use Feishu for Chinese community links (#16908)
Co-authored-by: Frank <frank@anoma.ly>
2026-03-11 11:07:13 +08:00
222 changed files with 3658 additions and 2987 deletions

View File

@@ -3,14 +3,6 @@ description: "Setup Bun with caching and install dependencies"
runs:
using: "composite"
steps:
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Get baseline download URL
id: bun-url
shell: bash
@@ -31,6 +23,19 @@ runs:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }}
- name: Get cache directory
id: cache
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install setuptools for distutils compatibility
run: python3 -m pip install setuptools || pip install setuptools || true
shell: bash

View File

@@ -115,6 +115,9 @@ jobs:
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: windows-2025
target: aarch64-pc-windows-msvc
- host: blacksmith-4vcpu-windows-2025
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
@@ -149,6 +152,10 @@ jobs:
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@v4
@@ -254,6 +261,10 @@ jobs:
- host: macos-latest
target: aarch64-apple-darwin
platform_flag: --mac --arm64
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: "windows-2025"
target: aarch64-pc-windows-msvc
platform_flag: --win --arm64
- host: "blacksmith-4vcpu-windows-2025"
target: x86_64-pc-windows-msvc
platform_flag: --win

View File

@@ -137,4 +137,4 @@ OpenCode 内置两种 Agent可用 `Tab` 键快速切换:
---
**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)

View File

@@ -137,4 +137,4 @@ OpenCode 內建了兩種 Agent您可以使用 `Tab` 鍵快速切換。
---
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true) | [X.com](https://x.com/opencode)

View File

@@ -46,6 +46,7 @@
"@solidjs/router": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"effect": "4.0.0-beta.29",
"fuzzysort": "catalog:",
"ghostty-web": "github:anomalyco/ghostty-web#main",
"luxon": "catalog:",
@@ -226,6 +227,7 @@
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"effect": "4.0.0-beta.29",
"electron-log": "^5",
"electron-store": "^10",
"electron-updater": "^6",
@@ -392,6 +394,7 @@
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "catalog:",

View File

@@ -103,6 +103,12 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
const zenLiteProduct = new stripe.Product("ZenLite", {
name: "OpenCode Go",
})
const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50", {
name: "First month 50% off",
percentOff: 50,
appliesToProducts: [zenLiteProduct.id],
duration: "once",
})
const zenLitePrice = new stripe.Price("ZenLitePrice", {
product: zenLiteProduct.id,
currency: "usd",
@@ -116,6 +122,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
properties: {
product: zenLiteProduct.id,
price: zenLitePrice.id,
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
},
})

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-dhL4YeSi4Lm9yDp919Fx7N2hyLUbZQa2qWoCf/50ce8=",
"aarch64-linux": "sha256-//YxCsrvYlxuvd0MtFFO+pLxjmuemyrvGzSIPxzO+rA=",
"aarch64-darwin": "sha256-c65kSWteQNaBcQUsjbXNqT61vt98JPNYo9yMNvUygCw=",
"x86_64-darwin": "sha256-hlTzEFv3nZHwlDXU65LfMC+NaqYjjyZqagdJ366CNxY="
"x86_64-linux": "sha256-5+9GAHej/EWz87Z3eTI9yBDRL1Ko0RoXsLo/Q3t42WA=",
"aarch64-linux": "sha256-4FWmoWkLKWKita3+XHZEiDy5grOQgdzOY1AZzb0TDWE=",
"aarch64-darwin": "sha256-L4FPB1E5AtV3V6qZjmX6YM7Q/mwSYlhYyZXPXAxrLFU=",
"x86_64-darwin": "sha256-bJCcrzDF2tIsKScxw5CoW+ZRUHe4KbUWLSqiR/M7vu8="
}
}

View File

@@ -70,6 +70,8 @@ test("test description", async ({ page, sdk, gotoSession }) => {
- `openSettings(page)` - Open settings dialog
- `closeDialog(page, dialog)` - Close any dialog
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
- `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output
- `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output
- `withSession(sdk, title, callback)` - Create temp session
- `withProject(...)` - Create temp project/workspace
- `sessionIDFromUrl(url)` - Read session ID from URL
@@ -167,6 +169,13 @@ await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
await page.keyboard.press(`${modKey}+Comma`) // Open settings
```
### Terminal Tests
- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
## Writing New Tests
1. Choose appropriate folder or create new one

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { execSync } from "node:child_process"
import { terminalAttr, type E2EWindow } from "../src/testing/terminal"
import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
import {
dropdownMenuTriggerSelector,
@@ -15,6 +16,7 @@ import {
listItemSelector,
listItemKeySelector,
listItemKeyStartsWithSelector,
terminalSelector,
workspaceItemSelector,
workspaceMenuTriggerSelector,
} from "./selectors"
@@ -28,6 +30,53 @@ export async function defocus(page: Page) {
.catch(() => undefined)
}
async function terminalID(term: Locator) {
const id = await term.getAttribute(terminalAttr)
if (id) return id
throw new Error(`Active terminal missing ${terminalAttr}`)
}
async function terminalReady(page: Page, term?: Locator) {
const next = term ?? page.locator(terminalSelector).first()
const id = await terminalID(next)
return page.evaluate((id) => {
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
return !!state?.connected && (state.settled ?? 0) > 0
}, id)
}
async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
const next = input.term ?? page.locator(terminalSelector).first()
const id = await terminalID(next)
return page.evaluate(
(input) => {
const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[input.id]
return state?.rendered.includes(input.token) ?? false
},
{ id, token: input.token },
)
}
export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
const term = input?.term ?? page.locator(terminalSelector).first()
const timeout = input?.timeout ?? 10_000
await expect(term).toBeVisible()
await expect(term.locator("textarea")).toHaveCount(1)
await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
}
export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
const term = input.term ?? page.locator(terminalSelector).first()
const timeout = input.timeout ?? 10_000
await waitTerminalReady(page, { term, timeout })
const textarea = term.locator("textarea")
await term.click()
await expect(textarea).toBeFocused()
await page.keyboard.type(input.cmd)
await page.keyboard.press("Enter")
await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
}
export async function openPalette(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+P`)

View File

@@ -1,4 +1,5 @@
import { test as base, expect, type Page } from "@playwright/test"
import type { E2EWindow } from "../src/testing/terminal"
import { cleanupSession, cleanupTestProject, createTestProject, seedProjects, sessionIDFromUrl } from "./actions"
import { promptSelector } from "./selectors"
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
@@ -91,6 +92,14 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
await seedProjects(page, input)
await page.addInitScript(() => {
const win = window as E2EWindow
win.__opencode_e2e = {
...win.__opencode_e2e,
terminal: {
enabled: true,
terminals: {},
},
}
localStorage.setItem(
"opencode.global.dat:model",
JSON.stringify({

View File

@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
import { waitTerminalReady } from "../actions"
import { promptSelector, terminalSelector } from "../selectors"
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
@@ -6,16 +7,29 @@ test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
const prompt = page.locator(promptSelector)
const terminal = page.locator(terminalSelector)
const slash = page.locator('[data-slash-id="terminal.toggle"]').first()
await expect(terminal).not.toBeVisible()
await prompt.fill("/terminal")
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
await expect(slash).toBeVisible()
await page.keyboard.press("Enter")
await expect(terminal).toBeVisible()
await waitTerminalReady(page, { term: terminal })
await prompt.fill("/terminal")
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
// Terminal panel retries focus (immediate, RAF, 120ms, 240ms) after opening,
// which can steal focus from the prompt and prevent fill() from triggering
// the slash popover. Re-attempt click+fill until all retries are exhausted
// and the popover appears.
await expect
.poll(
async () => {
await prompt.click().catch(() => false)
await prompt.fill("/terminal").catch(() => false)
return slash.isVisible().catch(() => false)
},
{ timeout: 10_000 },
)
.toBe(true)
await page.keyboard.press("Enter")
await expect(terminal).not.toBeVisible()
})

View File

@@ -0,0 +1,217 @@
import { waitSessionIdle, withSession } from "../actions"
import { test, expect } from "../fixtures"
import { createSdk } from "../utils"
const count = 14
function body(mark: string) {
return [
`title ${mark}`,
`mark ${mark}`,
...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`),
]
}
function files(tag: string) {
return Array.from({ length: count }, (_, i) => {
const id = String(i).padStart(2, "0")
return {
file: `review-scroll-${id}.txt`,
mark: `${tag}-${id}`,
}
})
}
function seed(list: ReturnType<typeof files>) {
const out = ["*** Begin Patch"]
for (const item of list) {
out.push(`*** Add File: ${item.file}`)
for (const line of body(item.mark)) out.push(`+${line}`)
}
out.push("*** End Patch")
return out.join("\n")
}
function edit(file: string, prev: string, next: string) {
return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join(
"\n",
)
}
async function patch(sdk: ReturnType<typeof createSdk>, sessionID: string, patchText: string) {
await sdk.session.promptAsync({
sessionID,
agent: "build",
system: [
"You are seeding deterministic e2e UI state.",
"Your only valid response is one apply_patch tool call.",
`Use this JSON input: ${JSON.stringify({ patchText })}`,
"Do not call any other tools.",
"Do not output plain text.",
].join("\n"),
parts: [{ type: "text", text: "Apply the provided patch exactly once." }],
})
await waitSessionIdle(sdk, sessionID, 120_000)
}
async function show(page: Parameters<typeof test>[0]["page"]) {
const btn = page.getByRole("button", { name: "Toggle review" }).first()
await expect(btn).toBeVisible()
if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click()
await expect(btn).toHaveAttribute("aria-expanded", "true")
}
async function expand(page: Parameters<typeof test>[0]["page"]) {
const close = page.getByRole("button", { name: /^Collapse all$/i }).first()
const open = await close
.isVisible()
.then((value) => value)
.catch(() => false)
const btn = page.getByRole("button", { name: /^Expand all$/i }).first()
if (open) {
await close.click()
await expect(btn).toBeVisible()
}
await expect(btn).toBeVisible()
await btn.click()
await expect(close).toBeVisible()
}
async function waitMark(page: Parameters<typeof test>[0]["page"], file: string, mark: string) {
await page.waitForFunction(
({ file, mark }) => {
const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
if (!(view instanceof HTMLElement)) return false
const head = Array.from(view.querySelectorAll("h3")).find(
(node) => node instanceof HTMLElement && node.textContent?.includes(file),
)
if (!(head instanceof HTMLElement)) return false
return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => {
if (!(host instanceof HTMLElement)) return false
const root = host.shadowRoot
return root?.textContent?.includes(`mark ${mark}`) ?? false
})
},
{ file, mark },
{ timeout: 60_000 },
)
}
async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
return page.evaluate((file) => {
const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
if (!(view instanceof HTMLElement)) return null
const row = Array.from(view.querySelectorAll("h3")).find(
(node) => node instanceof HTMLElement && node.textContent?.includes(file),
)
if (!(row instanceof HTMLElement)) return null
const a = row.getBoundingClientRect()
const b = view.getBoundingClientRect()
return {
top: a.top - b.top,
y: view.scrollTop,
}
}, file)
}
test("review keeps scroll position after a live diff update", async ({ page, withProject }) => {
test.skip(Boolean(process.env.CI), "Flaky in CI for now.")
test.setTimeout(180_000)
const tag = `review-${Date.now()}`
const list = files(tag)
const hit = list[list.length - 4]!
const next = `${tag}-live`
await page.setViewportSize({ width: 1600, height: 1000 })
await withProject(async (project) => {
const sdk = createSdk(project.directory)
await withSession(sdk, `e2e review ${tag}`, async (session) => {
await patch(sdk, session.id, seed(list))
await expect
.poll(
async () => {
const info = await sdk.session.get({ sessionID: session.id }).then((res) => res.data)
return info?.summary?.files ?? 0
},
{ timeout: 60_000 },
)
.toBe(list.length)
await expect
.poll(
async () => {
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
return diff.length
},
{ timeout: 60_000 },
)
.toBe(list.length)
await project.gotoSession(session.id)
await show(page)
const tab = page.getByRole("tab", { name: /Review/i }).first()
await expect(tab).toBeVisible()
await tab.click()
const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
await expect(view).toBeVisible()
const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ })
await expect(heads).toHaveCount(list.length, {
timeout: 60_000,
})
await expand(page)
await waitMark(page, hit.file, hit.mark)
const row = page
.getByRole("heading", { level: 3, name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) })
.first()
await expect(row).toBeVisible()
await row.evaluate((el) => el.scrollIntoView({ block: "center" }))
await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200)
const prev = await spot(page, hit.file)
if (!prev) throw new Error(`missing review row for ${hit.file}`)
await patch(sdk, session.id, edit(hit.file, hit.mark, next))
await expect
.poll(
async () => {
const diff = await sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
const item = diff.find((item) => item.file === hit.file)
return typeof item?.after === "string" ? item.after : ""
},
{ timeout: 60_000 },
)
.toContain(`mark ${next}`)
await waitMark(page, hit.file, next)
await expect
.poll(
async () => {
const next = await spot(page, hit.file)
if (!next) return Number.POSITIVE_INFINITY
return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y))
},
{ timeout: 60_000 },
)
.toBeLessThanOrEqual(32)
})
})
})

View File

@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { openSettings, closeDialog, withSession } from "../actions"
import { openSettings, closeDialog, waitTerminalReady, withSession } from "../actions"
import { keybindButtonSelector, terminalSelector } from "../selectors"
import { modKey } from "../utils"
@@ -302,7 +302,7 @@ test("changing terminal toggle keybind works", async ({ page, gotoSession }) =>
await expect(terminal).not.toBeVisible()
await page.keyboard.press(`${modKey}+Y`)
await expect(terminal).toBeVisible()
await waitTerminalReady(page, { term: terminal })
await page.keyboard.press(`${modKey}+Y`)
await expect(terminal).not.toBeVisible()

View File

@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
import { waitTerminalReady } from "../actions"
import { promptSelector, terminalSelector } from "../selectors"
import { terminalToggleKey } from "../utils"
@@ -13,8 +14,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await page.keyboard.press(terminalToggleKey)
}
await expect(terminals.first()).toBeVisible()
await expect(terminals.first().locator("textarea")).toHaveCount(1)
await waitTerminalReady(page, { term: terminals.first() })
await expect(terminals).toHaveCount(1)
// Ghostty captures a lot of keybinds when focused; move focus back
@@ -24,5 +24,5 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes
await expect(tabs).toHaveCount(2)
await expect(terminals).toHaveCount(1)
await expect(terminals.first().locator("textarea")).toHaveCount(1)
await waitTerminalReady(page, { term: terminals.first() })
})

View File

@@ -1,4 +1,5 @@
import type { Page } from "@playwright/test"
import { runTerminal, waitTerminalReady } from "../actions"
import { test, expect } from "../fixtures"
import { terminalSelector } from "../selectors"
import { terminalToggleKey, workspacePersistKey } from "../utils"
@@ -17,16 +18,7 @@ async function open(page: Page) {
const terminal = page.locator(terminalSelector)
const visible = await terminal.isVisible().catch(() => false)
if (!visible) await page.keyboard.press(terminalToggleKey)
await expect(terminal).toBeVisible()
await expect(terminal.locator("textarea")).toHaveCount(1)
}
async function run(page: Page, cmd: string) {
const terminal = page.locator(terminalSelector)
await expect(terminal).toBeVisible()
await terminal.click()
await page.keyboard.type(cmd)
await page.keyboard.press("Enter")
await waitTerminalReady(page, { term: terminal })
}
async function store(page: Page, key: string) {
@@ -56,15 +48,16 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
await gotoSession()
await open(page)
await run(page, `echo ${one}`)
await runTerminal(page, { cmd: `echo ${one}`, token: one })
await page.getByRole("button", { name: /new terminal/i }).click()
await expect(tabs).toHaveCount(2)
await run(page, `echo ${two}`)
await runTerminal(page, { cmd: `echo ${two}`, token: two })
await first.click()
await expect(first).toHaveAttribute("aria-selected", "true")
await expect
.poll(
async () => {
@@ -76,7 +69,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
second: second.includes(two),
}
},
{ timeout: 30_000 },
{ timeout: 5_000 },
)
.toEqual({ first: false, second: true })
@@ -93,7 +86,7 @@ test("inactive terminal tab buffers persist across tab switches", async ({ page,
second: second.includes(two),
}
},
{ timeout: 30_000 },
{ timeout: 5_000 },
)
.toEqual({ first: true, second: false })
})

View File

@@ -1,4 +1,5 @@
import { test, expect } from "../fixtures"
import { waitTerminalReady } from "../actions"
import { terminalSelector } from "../selectors"
import { terminalToggleKey } from "../utils"
@@ -13,5 +14,5 @@ test("terminal panel can be toggled", async ({ page, gotoSession }) => {
}
await page.keyboard.press(terminalToggleKey)
await expect(terminal).toBeVisible()
await waitTerminalReady(page, { term: terminal })
})

View File

@@ -45,8 +45,8 @@
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
@@ -56,6 +56,7 @@
"@solidjs/router": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"effect": "4.0.0-beta.29",
"fuzzysort": "catalog:",
"ghostty-web": "github:anomalyco/ghostty-web#main",
"luxon": "catalog:",

View File

@@ -1,14 +1,29 @@
import "@/index.css"
import { File } from "@opencode-ai/ui/file"
import { I18nProvider } from "@opencode-ai/ui/context"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { File } from "@opencode-ai/ui/file"
import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme"
import { MetaProvider } from "@solidjs/meta"
import { BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { Component, ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
import { type Duration, Effect } from "effect"
import {
type Component,
createResource,
createSignal,
ErrorBoundary,
For,
type JSX,
lazy,
onCleanup,
type ParentProps,
Show,
Suspense,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import { CommandProvider } from "@/context/command"
import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file"
@@ -22,13 +37,13 @@ import { NotificationProvider } from "@/context/notification"
import { PermissionProvider } from "@/context/permission"
import { usePlatform } from "@/context/platform"
import { PromptProvider } from "@/context/prompt"
import { type ServerConnection, ServerProvider, useServer } from "@/context/server"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { Dynamic } from "solid-js/web"
import { useCheckServerHealth } from "./utils/server-health"
const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
@@ -62,6 +77,9 @@ declare global {
deepLinks?: string[]
wsl?: boolean
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
}
}
}
@@ -115,7 +133,11 @@ export function AppBaseProviders(props: ParentProps) {
return (
<MetaProvider>
<Font />
<ThemeProvider>
<ThemeProvider
onThemeApplied={(_, mode) => {
void window.api?.setTitlebar?.({ mode })
}}
>
<LanguageProvider>
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
@@ -132,15 +154,108 @@ export function AppBaseProviders(props: ParentProps) {
)
}
function ServerKey(props: ParentProps) {
const effectMinDuration =
(duration: Duration.Input) =>
<A, E, R>(e: Effect.Effect<A, E, R>) =>
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
function ConnectionGate(props: ParentProps) {
const server = useServer()
const checkServerHealth = useCheckServerHealth()
const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking")
// performs repeated health check with a grace period for
// non-http connections, otherwise fails instantly
const [startupHealthCheck, healthCheckActions] = createResource(() =>
Effect.gen(function* () {
if (!server.current) return true
const { http, type } = server.current
while (true) {
const res = yield* Effect.promise(() => checkServerHealth(http))
if (res.healthy) return true
if (checkMode() === "background" || type === "http") return false
}
}).pipe(
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
Effect.runPromise,
),
)
return (
<Show when={server.key} keyed>
{props.children}
<Show
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
fallback={
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
</div>
}
>
<Show
when={startupHealthCheck()}
fallback={
<ConnectionError
onRetry={() => {
if (checkMode() === "background") healthCheckActions.refetch()
}}
onServerSelected={(key) => {
setCheckMode("blocking")
server.setActive(key)
healthCheckActions.refetch()
}}
/>
}
>
{props.children}
</Show>
</Show>
)
}
function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
const server = useServer()
const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
const timer = setInterval(() => props.onRetry?.(), 1000)
onCleanup(() => clearInterval(timer))
return (
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base gap-6 p-6">
<div class="flex flex-col items-center max-w-md text-center">
<Splash class="w-12 h-15 mb-4" />
<p class="text-14-regular text-text-base">
Could not reach <span class="text-text-strong font-medium">{server.name || server.key}</span>
</p>
<p class="mt-1 text-12-regular text-text-weak">Retrying automatically...</p>
</div>
<Show when={others().length > 0}>
<div class="flex flex-col gap-2 w-full max-w-sm">
<span class="text-12-regular text-text-base text-center">Other servers</span>
<div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2">
<For each={others()}>
{(conn) => {
const key = ServerConnection.key(conn)
return (
<button
type="button"
class="flex items-center gap-3 w-full px-3 py-2 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
onClick={() => props.onServerSelected?.(key)}
>
<span class="text-14-regular text-text-strong truncate">{serverName(conn)}</span>
</button>
)
}}
</For>
</div>
</div>
</Show>
</div>
)
}
export function AppInterface(props: {
children?: JSX.Element
defaultServer: ServerConnection.Key
@@ -149,7 +264,7 @@ export function AppInterface(props: {
}) {
return (
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
<ServerKey>
<ConnectionGate>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Dynamic
@@ -164,7 +279,7 @@ export function AppInterface(props: {
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
</ConnectionGate>
</ServerProvider>
)
}

View File

@@ -14,7 +14,9 @@ import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
const DEFAULT_USERNAME = "opencode"
interface ServerFormProps {
value: string
@@ -41,13 +43,15 @@ function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown
})
}
function useDefaultServer(platform: ReturnType<typeof usePlatform>, language: ReturnType<typeof useLanguage>) {
const [defaultUrl, defaultUrlActions] = createResource(
function useDefaultServer() {
const language = useLanguage()
const platform = usePlatform()
const [defaultKey, defaultUrlActions] = createResource(
async () => {
try {
const url = await platform.getDefaultServerUrl?.()
if (!url) return null
return normalizeServerUrl(url) ?? null
const key = await platform.getDefaultServer?.()
if (!key) return null
return key
} catch (err) {
showRequestError(language, err)
return null
@@ -56,20 +60,22 @@ function useDefaultServer(platform: ReturnType<typeof usePlatform>, language: Re
{ initialValue: null },
)
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
const setDefault = async (url: string | null) => {
const canDefault = createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer)
const setDefault = async (key: ServerConnection.Key | null) => {
try {
await platform.setDefaultServerUrl?.(url)
defaultUrlActions.mutate(url)
await platform.setDefaultServer?.(key)
defaultUrlActions.mutate(key)
} catch (err) {
showRequestError(language, err)
}
}
return { defaultUrl, canDefault, setDefault }
return { defaultKey, canDefault, setDefault }
}
function useServerPreview(fetcher: typeof fetch) {
function useServerPreview() {
const checkServerHealth = useCheckServerHealth()
const looksComplete = (value: string) => {
const normalized = normalizeServerUrl(value)
if (!normalized) return false
@@ -92,7 +98,7 @@ function useServerPreview(fetcher: typeof fetch) {
const http: ServerConnection.HttpBase = { url: normalized }
if (username) http.username = username
if (password) http.password = password
const result = await checkServerHealth(http, fetcher)
const result = await checkServerHealth(http)
setStatus(result.healthy)
}
@@ -170,15 +176,15 @@ export function DialogSelectServer() {
const server = useServer()
const platform = usePlatform()
const language = useLanguage()
const fetcher = platform.fetch ?? globalThis.fetch
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
const { previewStatus } = useServerPreview(fetcher)
const { defaultKey, canDefault, setDefault } = useDefaultServer()
const { previewStatus } = useServerPreview()
const checkServerHealth = useCheckServerHealth()
const [store, setStore] = createStore({
status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
addServer: {
url: "",
name: "",
username: "",
username: DEFAULT_USERNAME,
password: "",
adding: false,
error: "",
@@ -201,7 +207,7 @@ export function DialogSelectServer() {
setStore("addServer", {
url: "",
name: "",
username: "",
username: DEFAULT_USERNAME,
password: "",
adding: false,
error: "",
@@ -264,7 +270,7 @@ export function DialogSelectServer() {
const results: Record<ServerConnection.Key, ServerHealth> = {}
await Promise.all(
items().map(async (conn) => {
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
}),
)
setStore("status", reconcile(results))
@@ -362,9 +368,9 @@ export function DialogSelectServer() {
http: { url: normalized },
}
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
if (store.addServer.username) conn.http.username = store.addServer.username
if (store.addServer.password) conn.http.password = store.addServer.password
const result = await checkServerHealth(conn.http, fetcher)
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
const result = await checkServerHealth(conn.http)
setStore("addServer", { adding: false })
if (!result.healthy) {
setStore("addServer", { error: language.t("dialog.server.add.error") })
@@ -404,7 +410,7 @@ export function DialogSelectServer() {
displayName: name,
http: { url: normalized, username, password },
}
const result = await checkServerHealth(conn.http, fetcher)
const result = await checkServerHealth(conn.http)
setStore("editServer", { busy: false })
if (!result.healthy) {
setStore("editServer", { error: language.t("dialog.server.add.error") })
@@ -441,7 +447,7 @@ export function DialogSelectServer() {
showForm: true,
url: "",
name: "",
username: "",
username: DEFAULT_USERNAME,
password: "",
error: "",
status: undefined,
@@ -494,8 +500,8 @@ export function DialogSelectServer() {
async function handleRemove(url: ServerConnection.Key) {
server.remove(url)
if ((await platform.getDefaultServerUrl?.()) === url) {
platform.setDefaultServerUrl?.(null)
if ((await platform.getDefaultServer?.()) === url) {
platform.setDefaultServer?.(null)
}
}
@@ -551,7 +557,7 @@ export function DialogSelectServer() {
status={store.status[key]}
class="flex items-center gap-3 min-w-0 flex-1"
badge={
<Show when={defaultUrl() === i.http.url}>
<Show when={defaultKey() === ServerConnection.key(i)}>
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
{language.t("dialog.server.status.default")}
</span>
@@ -584,14 +590,14 @@ export function DialogSelectServer() {
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={canDefault() && defaultUrl() !== i.http.url}>
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
<Show when={canDefault() && defaultKey() !== key}>
<DropdownMenu.Item onSelect={() => setDefault(key)}>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.default")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<Show when={canDefault() && defaultUrl() === i.http.url}>
<Show when={canDefault() && defaultKey() === key}>
<DropdownMenu.Item onSelect={() => setDefault(null)}>
<DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.defaultRemove")}

View File

@@ -490,6 +490,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setComposing(false)
}
const handleCompositionStart = () => {
setComposing(true)
}
const handleCompositionEnd = () => {
setComposing(false)
requestAnimationFrame(() => {
if (composing()) return
reconcile(prompt.current().filter((part) => part.type !== "image"))
})
}
const agentList = createMemo(() =>
sync.data.agent
.filter((agent) => !agent.hidden && agent.mode !== "primary")
@@ -680,24 +692,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const reconcile = (input: Prompt) => {
if (mirror.input) {
mirror.input = false
if (isNormalizedEditor()) return
renderEditorWithCursor(input)
return
}
const dom = parseFromDOM()
if (isNormalizedEditor() && isPromptEqual(input, dom)) return
renderEditorWithCursor(input)
}
createEffect(
on(
() => prompt.current(),
(currentParts) => {
const inputParts = currentParts.filter((part) => part.type !== "image")
if (mirror.input) {
mirror.input = false
if (isNormalizedEditor()) return
renderEditorWithCursor(inputParts)
return
}
const domParts = parseFromDOM()
if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
renderEditorWithCursor(inputParts)
(parts) => {
if (composing()) return
reconcile(parts.filter((part) => part.type !== "image"))
},
),
)
@@ -1208,8 +1223,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
spellcheck={store.mode === "normal"}
onInput={handleInput}
onPaste={handlePaste}
onCompositionStart={() => setComposing(true)}
onCompositionEnd={() => setComposing(false)}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
classList={{

View File

@@ -14,7 +14,7 @@ import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
import { DialogSelectServer } from "./dialog-select-server"
const pollMs = 10_000
@@ -53,7 +53,8 @@ const listServersByHealth = (
})
}
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, fetcher: typeof fetch) => {
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
const checkServerHealth = useCheckServerHealth()
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
createEffect(() => {
@@ -64,7 +65,7 @@ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, fetcher: typ
const results: Record<string, ServerHealth> = {}
await Promise.all(
list.map(async (conn) => {
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
}),
)
if (dead) return
@@ -168,7 +169,6 @@ export function StatusPopover() {
const language = useLanguage()
const navigate = useNavigate()
const fetcher = platform.fetch ?? globalThis.fetch
const servers = createMemo(() => {
const current = server.current
const list = server.list
@@ -176,10 +176,10 @@ export function StatusPopover() {
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
})
const health = useServerHealth(servers, fetcher)
const health = useServerHealth(servers)
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
const mcp = useMcpToggle({ sync, sdk, language })
const defaultServer = useDefaultServerKey(platform.getDefaultServerUrl)
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)

View File

@@ -10,6 +10,7 @@ import { useSDK } from "@/context/sdk"
import { useServer } from "@/context/server"
import { monoFontFamily, useSettings } from "@/context/settings"
import type { LocalPTY } from "@/context/terminal"
import { terminalAttr, terminalProbe } from "@/testing/terminal"
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
import { terminalWriter } from "@/utils/terminal-writer"
@@ -160,6 +161,7 @@ export const Terminal = (props: TerminalProps) => {
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
const id = local.pty.id
const probe = terminalProbe(id)
const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
const restoreSize =
restore &&
@@ -326,6 +328,9 @@ export const Terminal = (props: TerminalProps) => {
}
onMount(() => {
probe.init()
cleanups.push(() => probe.drop())
const run = async () => {
const loaded = await loadGhostty()
if (disposed) return
@@ -353,7 +358,13 @@ export const Terminal = (props: TerminalProps) => {
}
ghostty = g
term = t
output = terminalWriter((data, done) => t.write(data, done))
output = terminalWriter((data, done) =>
t.write(data, () => {
probe.render(data)
probe.settle()
done?.()
}),
)
t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
@@ -441,10 +452,6 @@ export const Terminal = (props: TerminalProps) => {
startResize()
}
// t.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
const once = { value: false }
let closing = false
@@ -452,7 +459,7 @@ export const Terminal = (props: TerminalProps) => {
url.searchParams.set("directory", sdk.directory)
url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0))
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
url.username = server.current?.http.username ?? ""
url.username = server.current?.http.username ?? "opencode"
url.password = server.current?.http.password ?? ""
const socket = new WebSocket(url)
@@ -460,6 +467,7 @@ export const Terminal = (props: TerminalProps) => {
ws = socket
const handleOpen = () => {
probe.connect()
local.onConnect?.()
scheduleSize(t.cols, t.rows)
}
@@ -560,6 +568,7 @@ export const Terminal = (props: TerminalProps) => {
<div
ref={container}
data-component="terminal"
{...{ [terminalAttr]: id }}
data-prevent-autofocus
tabIndex={-1}
style={{ "background-color": terminalColors().background }}

View File

@@ -1,4 +1,4 @@
import { createEffect, createMemo, Show, untrack } from "solid-js"
import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -282,7 +282,7 @@ export function Titlebar() {
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
<div class="w-6 shrink-0" />
{!tauriApi() && <div class="w-36 shrink-0" />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>

View File

@@ -1,6 +1,7 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
import type { Accessor } from "solid-js"
import { ServerConnection } from "./server"
type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
@@ -58,10 +59,10 @@ export type Platform = {
fetch?: typeof fetch
/** Get the configured default server URL (platform-specific) */
getDefaultServerUrl?(): Promise<string | null>
getDefaultServer?(): Promise<ServerConnection.Key | null>
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServerUrl?(url: string | null): Promise<void> | void
setDefaultServer?(url: ServerConnection.Key | null): Promise<void> | void
/** Get the configured WSL integration (desktop only) */
getWslEnabled?(): Promise<boolean>

View File

@@ -1,9 +1,8 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { Persist, persisted } from "@/utils/persist"
import { checkServerHealth } from "@/utils/server-health"
import { useCheckServerHealth } from "@/utils/server-health"
type StoredProject = { worktree: string; expanded: boolean }
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
@@ -96,7 +95,7 @@ export namespace ServerConnection {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
const platform = usePlatform()
const checkServerHealth = useCheckServerHealth()
const [store, setStore, _, ready] = persisted(
Persist.global("server", ["server.v3"]),
@@ -197,8 +196,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const isReady = createMemo(() => ready() && !!state.active)
const fetcher = platform.fetch ?? globalThis.fetch
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http, fetcher).then((x) => x.healthy)
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
createEffect(() => {
const current_ = current()

View File

@@ -98,6 +98,19 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
throw new Error(getRootNotFoundError())
}
const getCurrentUrl = () => {
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin
}
const getDefaultUrl = () => {
const lsDefault = readDefaultServerUrl()
if (lsDefault) return lsDefault
return getCurrentUrl()
}
const platform: Platform = {
platform: "web",
version: pkg.version,
@@ -106,26 +119,20 @@ const platform: Platform = {
forward,
restart,
notify,
getDefaultServerUrl: async () => readDefaultServerUrl(),
setDefaultServerUrl: writeDefaultServerUrl,
getDefaultServer: async () => {
const stored = readDefaultServerUrl()
return stored ? ServerConnection.Key.make(stored) : null
},
setDefaultServer: writeDefaultServerUrl,
}
const defaultUrl = iife(() => {
const lsDefault = readDefaultServerUrl()
if (lsDefault) return lsDefault
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin
})
if (root instanceof HTMLElement) {
const server: ServerConnection.Http = { type: "http", http: { url: defaultUrl } }
const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } }
render(
() => (
<PlatformProvider value={platform}>
<AppBaseProviders>
<AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} />
<AppInterface defaultServer={ServerConnection.Key.make(getDefaultUrl())} servers={[server]} />
</AppBaseProviders>
</PlatformProvider>
),

View File

@@ -530,6 +530,11 @@ export const dict = {
"session.todo.title": "Todos",
"session.todo.collapse": "Collapse",
"session.todo.expand": "Expand",
"session.revertDock.summary.one": "{{count}} rolled back message",
"session.revertDock.summary.other": "{{count}} rolled back messages",
"session.revertDock.collapse": "Collapse rolled back messages",
"session.revertDock.expand": "Expand rolled back messages",
"session.revertDock.restore": "Restore message",
"session.new.title": "Build anything",
"session.new.worktree.main": "Main branch",

View File

@@ -43,6 +43,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { extractPromptFromParts } from "@/utils/prompt"
import { same } from "@/utils/same"
import { formatServerError } from "@/utils/server-errors"
@@ -286,6 +287,7 @@ export default function Page() {
const [ui, setUi] = createStore({
git: false,
pendingMessage: undefined as string | undefined,
restoring: undefined as string | undefined,
reviewSnap: false,
scrollGesture: 0,
scroll: {
@@ -862,6 +864,36 @@ export default function Page() {
</div>
)
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
if (store.changes === "turn") return emptyTurn()
if (hasReview() && !diffsReady()) {
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
}
if (reviewEmptyKey() === "session.review.noVcs") {
return (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
<div class="text-14-medium text-text-strong">Create a Git repository</div>
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
Track, review, and undo changes in this project
</div>
</div>
<Button size="large" disabled={ui.git} onClick={initGit}>
{ui.git ? "Creating Git repository..." : "Create Git repository"}
</Button>
</div>
)
}
return (
<div class={input.emptyClass}>
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
</div>
)
}
const reviewContent = (input: {
diffStyle: DiffStyle
onDiffStyleChange?: (style: DiffStyle) => void
@@ -870,98 +902,25 @@ export default function Page() {
emptyClass: string
}) => (
<Show when={!store.deferRender}>
<Switch>
<Match when={store.changes === "turn" && !!params.id}>
<SessionReviewTab
title={changesTitle()}
empty={emptyTurn()}
diffs={reviewDiffs}
view={view}
diffStyle={input.diffStyle}
onDiffStyleChange={input.onDiffStyleChange}
onScrollRef={(el) => setTree("reviewScroll", el)}
focusedFile={tree.activeDiff}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
onLineCommentUpdate={updateCommentInContext}
onLineCommentDelete={removeCommentFromContext}
lineCommentActions={reviewCommentActions()}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={openReviewFile}
classes={input.classes}
/>
</Match>
<Match when={hasReview()}>
<Show
when={diffsReady()}
fallback={<div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>}
>
<SessionReviewTab
title={changesTitle()}
diffs={reviewDiffs}
view={view}
diffStyle={input.diffStyle}
onDiffStyleChange={input.onDiffStyleChange}
onScrollRef={(el) => setTree("reviewScroll", el)}
focusedFile={tree.activeDiff}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
onLineCommentUpdate={updateCommentInContext}
onLineCommentDelete={removeCommentFromContext}
lineCommentActions={reviewCommentActions()}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={openReviewFile}
classes={input.classes}
/>
</Show>
</Match>
<Match when={true}>
<SessionReviewTab
title={changesTitle()}
empty={
store.changes === "turn" ? (
emptyTurn()
) : reviewEmptyKey() === "session.review.noVcs" ? (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
<div class="text-14-medium text-text-strong">Create a Git repository</div>
<div
class="text-14-regular text-text-base max-w-md"
style={{ "line-height": "var(--line-height-normal)" }}
>
Track, review, and undo changes in this project
</div>
</div>
<Button size="large" disabled={ui.git} onClick={initGit}>
{ui.git ? "Creating Git repository..." : "Create Git repository"}
</Button>
</div>
) : (
<div class={input.emptyClass}>
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
</div>
)
}
diffs={reviewDiffs}
view={view}
diffStyle={input.diffStyle}
onDiffStyleChange={input.onDiffStyleChange}
onScrollRef={(el) => setTree("reviewScroll", el)}
focusedFile={tree.activeDiff}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
onLineCommentUpdate={updateCommentInContext}
onLineCommentDelete={removeCommentFromContext}
lineCommentActions={reviewCommentActions()}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={openReviewFile}
classes={input.classes}
/>
</Match>
</Switch>
<SessionReviewTab
title={changesTitle()}
empty={reviewEmpty(input)}
diffs={reviewDiffs}
view={view}
diffStyle={input.diffStyle}
onDiffStyleChange={input.onDiffStyleChange}
onScrollRef={(el) => setTree("reviewScroll", el)}
focusedFile={tree.activeDiff}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
onLineCommentUpdate={updateCommentInContext}
onLineCommentDelete={removeCommentFromContext}
lineCommentActions={reviewCommentActions()}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={openReviewFile}
classes={input.classes}
/>
</Show>
)
@@ -1222,6 +1181,110 @@ export default function Page() {
scroller: () => scroller,
})
const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], {
directory: sdk.directory,
attachmentName: language.t("common.attachment"),
})
const line = (id: string) => {
const text = draft(id)
.map((part) => (part.type === "image" ? `[image:${part.filename}]` : part.content))
.join("")
.replace(/\s+/g, " ")
.trim()
if (text) return text
return `[${language.t("common.attachment")}]`
}
const fail = (err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: formatServerError(err, language.t),
})
}
const busy = (sessionID: string) => {
if (sync.data.session_status[sessionID]?.type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
}
const halt = (sessionID: string) =>
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
const fork = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return sdk.client.session
.fork(input)
.then((result) => {
const next = result.data
if (!next) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
})
return
}
navigate(`/${base64Encode(sdk.directory)}/session/${next.id}`)
requestAnimationFrame(() => {
prompt.set(value)
})
})
.catch(fail)
}
const revert = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then(() => {
prompt.set(value)
})
.catch(fail)
}
const restore = (id: string) => {
const sessionID = params.id
if (!sessionID || ui.restoring) return
const next = userMessages().find((item) => item.id > id)
setUi("restoring", id)
const task = !next
? halt(sessionID)
.then(() => sdk.client.session.unrevert({ sessionID }))
.then(() => {
prompt.reset()
})
: halt(sessionID)
.then(() =>
sdk.client.session.revert({
sessionID,
messageID: next.id,
}),
)
.then(() => {
prompt.set(draft(next.id))
})
return task.catch(fail).finally(() => {
setUi("restoring", (value) => (value === id ? undefined : value))
})
}
const rolled = createMemo(() => {
const id = revertMessageID()
if (!id) return []
return userMessages()
.filter((item) => item.id >= id)
.map((item) => ({ id: item.id, text: line(item.id) }))
})
const actions = { fork, revert }
createResizeObserver(
() => promptDock,
({ height }) => {
@@ -1311,6 +1374,7 @@ export default function Page() {
loadingClass: "px-4 py-4 text-text-weak",
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
})}
actions={actions}
scroll={ui.scroll}
onResumeScroll={resumeScroll}
setScrollRef={setScrollRef}
@@ -1376,6 +1440,15 @@ export default function Page() {
resumeScroll()
}}
onResponseSubmit={resumeScroll}
revert={
rolled().length > 0
? {
items: rolled(),
restoring: ui.restoring,
onRestore: restore,
}
: undefined
}
setPromptDockRef={(el) => {
promptDock = el
}}

View File

@@ -0,0 +1,10 @@
export const todoState = (input: {
count: number
done: boolean
live: boolean
}): "hide" | "clear" | "open" | "close" => {
if (input.count === 0) return "hide"
if (!input.live) return "clear"
if (!input.done) return "open"
return "close"
}

View File

@@ -8,6 +8,7 @@ import { usePrompt } from "@/context/prompt"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
@@ -20,6 +21,11 @@ export function SessionComposerRegion(props: {
onNewSessionWorktreeReset: () => void
onSubmit: () => void
onResponseSubmit: () => void
revert?: {
items: { id: string; text: string }[]
restoring?: string
onRestore: (id: string) => void
}
setPromptDockRef: (el: HTMLDivElement) => void
visualDuration?: number
bounce?: number
@@ -116,6 +122,8 @@ export function SessionComposerRegion(props: {
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
const [height, setHeight] = createSignal(320)
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
const full = createMemo(() => Math.max(78, height()))
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
@@ -170,9 +178,22 @@ export function SessionComposerRegion(props: {
<Show
when={prompt.ready()}
fallback={
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
{handoffPrompt() || language.t("prompt.loading")}
</div>
<>
<Show when={rolled()} keyed>
{(revert) => (
<div class="pb-2">
<SessionRevertDock
items={revert.items}
restoring={revert.restoring}
onRestore={revert.onRestore}
/>
</div>
)}
</Show>
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none">
{handoffPrompt() || language.t("prompt.loading")}
</div>
</>
}
>
<Show when={dock()}>
@@ -209,12 +230,23 @@ export function SessionComposerRegion(props: {
</div>
</div>
</Show>
<Show when={rolled()} keyed>
{(revert) => (
<div
style={{
"margin-top": `${-36 * value()}px`,
}}
>
<SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
</div>
)}
</Show>
<div
classList={{
"relative z-10": true,
}}
style={{
"margin-top": `${-36 * value()}px`,
"margin-top": `${-lift()}px`,
}}
>
<PromptInput

View File

@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { todoState } from "./session-composer-helpers"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
const session = (input: { id: string; parentID?: string }) =>
@@ -103,3 +104,25 @@ describe("sessionQuestionRequest", () => {
expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
})
})
describe("todoState", () => {
test("hides when there are no todos", () => {
expect(todoState({ count: 0, done: false, live: true })).toBe("hide")
})
test("opens while the session is still working", () => {
expect(todoState({ count: 2, done: false, live: true })).toBe("open")
})
test("closes completed todos after a running turn", () => {
expect(todoState({ count: 2, done: true, live: true })).toBe("close")
})
test("clears stale todos when the turn ends", () => {
expect(todoState({ count: 2, done: false, live: false })).toBe("clear")
})
test("clears completed todos when the session is no longer live", () => {
expect(todoState({ count: 2, done: true, live: false })).toBe("clear")
})
})

View File

@@ -8,8 +8,11 @@ import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { todoState } from "./session-composer-helpers"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
const idle = { type: "idle" as const }
export function createSessionComposerBlocked() {
const params = useParams()
const permission = usePermission()
@@ -59,9 +62,22 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
return globalSync.data.session_todo[id] ?? []
})
const done = createMemo(
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
)
const status = createMemo(() => {
const id = params.id
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const busy = createMemo(() => status().type !== "idle")
const live = createMemo(() => busy() || blocked())
const [store, setStore] = createStore({
responding: undefined as string | undefined,
dock: todos().length > 0,
dock: todos().length > 0 && live(),
closing: false,
opening: false,
})
@@ -89,10 +105,6 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
})
}
const done = createMemo(
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
)
let timer: number | undefined
let raf: number | undefined
@@ -111,21 +123,42 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
}, closeMs())
}
// Keep stale turn todos from reopening if the model never clears them.
const clear = () => {
const id = params.id
if (!id) return
globalSync.todo.set(id, [])
sync.set("todo", id, [])
}
createEffect(
on(
() => [todos().length, done()] as const,
([count, complete], prev) => {
() => [todos().length, done(), live()] as const,
([count, complete, active]) => {
if (raf) cancelAnimationFrame(raf)
raf = undefined
if (count === 0) {
const next = todoState({
count,
done: complete,
live: active,
})
if (next === "hide") {
if (timer) window.clearTimeout(timer)
timer = undefined
setStore({ dock: false, closing: false, opening: false })
return
}
if (!complete) {
if (next === "clear") {
if (timer) window.clearTimeout(timer)
timer = undefined
clear()
return
}
if (next === "open") {
if (timer) window.clearTimeout(timer)
timer = undefined
const hidden = !store.dock || store.closing
@@ -142,13 +175,8 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
return
}
if (prev && prev[1]) {
if (store.closing && !timer) scheduleClose()
return
}
setStore({ dock: true, opening: false, closing: true })
scheduleClose()
if (!timer) scheduleClose()
},
),
)

View File

@@ -0,0 +1,92 @@
import { For, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { DockTray } from "@opencode-ai/ui/dock-surface"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useLanguage } from "@/context/language"
export function SessionRevertDock(props: {
items: { id: string; text: string }[]
restoring?: string
onRestore: (id: string) => void
}) {
const language = useLanguage()
const [store, setStore] = createStore({
collapsed: false,
})
const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.items.length)
const label = createMemo(() =>
language.t(total() === 1 ? "session.revertDock.summary.one" : "session.revertDock.summary.other", {
count: total(),
}),
)
const preview = createMemo(() => props.items[0]?.text ?? "")
return (
<DockTray data-component="session-revert-dock">
<div
class="pl-3 pr-2 py-2 flex items-center gap-2"
role="button"
tabIndex={0}
onClick={toggle}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
toggle()
}}
>
<span class="shrink-0 text-14-regular text-text-strong cursor-default">{label()}</span>
<Show when={store.collapsed && preview()}>
<span class="min-w-0 flex-1 truncate text-14-regular text-text-base cursor-default">{preview()}</span>
</Show>
<div class="ml-auto shrink-0">
<IconButton
data-collapsed={store.collapsed ? "true" : "false"}
icon="chevron-down"
size="normal"
variant="ghost"
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={(event) => {
event.stopPropagation()
toggle()
}}
aria-label={
store.collapsed ? language.t("session.revertDock.expand") : language.t("session.revertDock.collapse")
}
/>
</div>
</div>
<Show when={store.collapsed}>
<div class="h-5" aria-hidden="true" />
</Show>
<Show when={!store.collapsed}>
<div class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
<For each={props.items}>
{(item) => (
<div class="flex items-center gap-2 min-w-0 rounded-[10px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
<Button
size="small"
variant="secondary"
class="shrink-0"
disabled={!!props.restoring}
onClick={() => props.onRestore(item.id)}
>
{language.t("session.revertDock.restore")}
</Button>
</div>
)}
</For>
</div>
</Show>
</DockTray>
)
}

View File

@@ -36,6 +36,11 @@ type MessageComment = {
const emptyMessages: MessageType[] = []
const idle = { type: "idle" as const }
type UserActions = {
fork?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
revert?: (input: { sessionID: string; messageID: string }) => Promise<void> | void
}
const messageComments = (parts: Part[]): MessageComment[] =>
parts.flatMap((part) => {
if (part.type !== "text" || !(part as TextPart).synthetic) return []
@@ -186,6 +191,7 @@ function createTimelineStaging(input: TimelineStageInput) {
export function MessageTimeline(props: {
mobileChanges: boolean
mobileFallback: JSX.Element
actions?: UserActions
scroll: { overflow: boolean; bottom: boolean }
onResumeScroll: () => void
setScrollRef: (el: HTMLDivElement | undefined) => void
@@ -764,6 +770,7 @@ export function MessageTimeline(props: {
"min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
}}
style={{ "content-visibility": "auto", "contain-intrinsic-size": "auto 500px" }}
>
<Show when={commentCount() > 0}>
<div class="w-full px-4 md:px-5 pb-2">
@@ -773,27 +780,31 @@ export function MessageTimeline(props: {
{(commentAccessor: () => MessageComment) => {
const comment = createMemo(() => commentAccessor())
return (
<div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
<FileIcon
node={{ path: comment().path, type: "file" }}
class="size-3.5 shrink-0"
/>
<span class="truncate">{getFilename(comment().path)}</span>
<Show when={comment().selection}>
{(selection) => (
<span class="shrink-0 text-text-weak">
{selection().startLine === selection().endLine
? `:${selection().startLine}`
: `:${selection().startLine}-${selection().endLine}`}
</span>
)}
</Show>
</div>
<div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
{comment().comment}
</div>
</div>
<Show when={comment()}>
{(c) => (
<div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
<div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
<FileIcon
node={{ path: c().path, type: "file" }}
class="size-3.5 shrink-0"
/>
<span class="truncate">{getFilename(c().path)}</span>
<Show when={c().selection}>
{(selection) => (
<span class="shrink-0 text-text-weak">
{selection().startLine === selection().endLine
? `:${selection().startLine}`
: `:${selection().startLine}-${selection().endLine}`}
</span>
)}
</Show>
</div>
<div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
{c().comment}
</div>
</div>
)}
</Show>
)
}}
</Index>
@@ -804,6 +815,7 @@ export function MessageTimeline(props: {
<SessionTurn
sessionID={sessionID() ?? ""}
messageID={messageID}
actions={props.actions}
active={active()}
queued={queued()}
status={active() ? sessionStatus() : undefined}

View File

@@ -0,0 +1,64 @@
export const terminalAttr = "data-pty-id"
export type TerminalProbeState = {
connected: boolean
rendered: string
settled: number
}
export type E2EWindow = Window & {
__opencode_e2e?: {
terminal?: {
enabled?: boolean
terminals?: Record<string, TerminalProbeState>
}
}
}
const seed = (): TerminalProbeState => ({
connected: false,
rendered: "",
settled: 0,
})
const root = () => {
if (typeof window === "undefined") return
const state = (window as E2EWindow).__opencode_e2e?.terminal
if (!state?.enabled) return
state.terminals ??= {}
return state.terminals
}
export const terminalProbe = (id: string) => {
const set = (next: Partial<TerminalProbeState>) => {
const terms = root()
if (!terms) return
terms[id] = { ...(terms[id] ?? seed()), ...next }
}
return {
init() {
set(seed())
},
connect() {
set({ connected: true })
},
render(data: string) {
const terms = root()
if (!terms) return
const prev = terms[id] ?? seed()
terms[id] = { ...prev, rendered: prev.rendered + data }
},
settle() {
const terms = root()
if (!terms) return
const prev = terms[id] ?? seed()
terms[id] = { ...prev, settled: prev.settled + 1 }
},
drop() {
const terms = root()
if (!terms) return
delete terms[id]
},
}
}

View File

@@ -1,3 +1,4 @@
import { usePlatform } from "@/context/platform"
import type { ServerConnection } from "@/context/server"
import { createSdkForServer } from "./server"
@@ -81,3 +82,10 @@ export async function checkServerHealth(
.catch((error) => next(count, error))
return attempt(0).finally(() => timeout?.clear?.())
}
export function useCheckServerHealth() {
const platform = usePlatform()
const fetcher = platform.fetch ?? globalThis.fetch
return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher)
}

View File

@@ -8,6 +8,12 @@ import { useI18n } from "~/context/i18n"
export function Footer() {
const language = useLanguage()
const i18n = useI18n()
const community = createMemo(() => {
const locale = language.locale()
return locale === "zh" || locale === "zht"
? ({ key: "footer.feishu", link: language.route("/feishu") } as const)
: ({ key: "footer.discord", link: language.route("/discord") } as const)
})
const githubData = createAsync(() => github())
const starCount = createMemo(() =>
githubData()?.stars
@@ -32,7 +38,7 @@ export function Footer() {
<a href={language.route("/changelog")}>{i18n.t("footer.changelog")}</a>
</div>
<div data-slot="cell">
<a href={language.route("/discord")}>{i18n.t("footer.discord")}</a>
<a href={community().link}>{i18n.t(community().key)}</a>
</div>
<div data-slot="cell">
<a href={config.social.twitter}>{i18n.t("footer.x")}</a>

View File

@@ -161,16 +161,12 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo
<li>
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
</li>
<Show when={!props.zen}>
<li>
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
</li>
</Show>
<Show when={!props.go}>
<li>
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
</li>
</Show>
<li>
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
</li>
<li>
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
</li>
<li>
<A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
</li>

View File

@@ -1,69 +1,25 @@
import { JSX } from "solid-js"
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconZen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg width="64" height="32" viewBox="0 0 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 9.14333V4.5719H4.57143V9.14333H0Z" fill="currentColor" />
<path d="M4.57178 9.14333V4.5719H9.14321V9.14333H4.57178Z" fill="currentColor" />
<path d="M9.1438 9.14333V4.5719H13.7152V9.14333H9.1438Z" fill="currentColor" />
<path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
<path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
<path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
<rect width="4.57143" height="4.57143" transform="translate(4.57178 13.7141)" fill="currentColor" />
<path d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z" fill="currentColor" fill-opacity="0.2" />
<path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
<path d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z" fill="currentColor" fill-opacity="0.2" />
<rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
<path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
<rect
width="4.57143"
height="4.57143"
transform="translate(4.57178 18.2859)"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
<path d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" fill="currentColor" fill-opacity="0.2" />
<path d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z" fill="currentColor" fill-opacity="0.2" />
<path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
<path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
<path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
<path d="M13.7124 27.4292V22.8578H18.2838V27.4292H13.7124Z" fill="currentColor" />
<path d="M22.8572 9.14333V4.5719H27.4286V9.14333H22.8572Z" fill="currentColor" />
<path d="M27.426 9.14333V4.5719H31.9975V9.14333H27.426Z" fill="currentColor" />
<path d="M32.001 9.14333V4.5719H36.5724V9.14333H32.001Z" fill="currentColor" />
<path d="M36.5698 9.14333V4.5719H41.1413V9.14333H36.5698Z" fill="currentColor" />
<path d="M22.8572 13.7152V9.1438H27.4286V13.7152H22.8572Z" fill="currentColor" />
<path d="M36.5698 13.7152V9.1438H41.1413V13.7152H36.5698Z" fill="currentColor" />
<path d="M22.8572 18.2855V13.7141H27.4286V18.2855H22.8572Z" fill="currentColor" />
<path d="M27.4292 18.2855V13.7141H32.0006V18.2855H27.4292Z" fill="currentColor" />
<path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
<path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
<path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
<path d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" fill="currentColor" fill-opacity="0.2" />
<path d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z" fill="currentColor" fill-opacity="0.2" />
<path d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z" fill="currentColor" fill-opacity="0.2" />
<path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
<path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
<path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
<path d="M36.5698 27.4292V22.8578H41.1413V27.4292H36.5698Z" fill="currentColor" />
<path d="M45.7144 9.14333V4.5719H50.2858V9.14333H45.7144Z" fill="currentColor" />
<path d="M50.2861 9.14333V4.5719H54.8576V9.14333H50.2861Z" fill="currentColor" />
<path d="M54.855 9.14333V4.5719H59.4264V9.14333H54.855Z" fill="currentColor" />
<path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
<path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
<path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
<path d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
<path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
<path d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
<path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
<path d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H6V18H18V12H24V24ZM6 18H0V12H6V18Z" fill="currentColor" fill-opacity="0.2" />
<path d="M6 24H24V30H0V18H6V24ZM18 18H6V12H18V18ZM24 12H18V6H0V0H24V12Z" fill="currentColor" />
<path d="M54 18V24H36V18H54Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54 18H36V24H54V30H30V0H54V18ZM36 12H48V6H36V12Z" fill="currentColor" />
<path d="M78 30H66V12H78V30Z" fill="currentColor" fill-opacity="0.2" />
<path d="M78 6H66V30H60V0H78V6ZM84 30H78V6H84V30Z" fill="currentColor" />
</svg>
)
}
export function IconGo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="currentColor" />
<path d="M12 18H18V24H6V12H12V18Z" fill="currentColor" fill-opacity="0.2" />
<path d="M48 12V24H36V12H48Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="currentColor" />
</svg>
)
}
@@ -111,6 +67,15 @@ export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconAlipay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M2.541 0H13.5a2.55 2.55 0 0 1 2.54 2.563v8.297c-.006 0-.531-.046-2.978-.813-.412-.14-.916-.327-1.479-.536q-.456-.17-.957-.353a13 13 0 0 0 1.325-3.373H8.822V4.649h3.831v-.634h-3.83V2.121H7.26c-.274 0-.274.273-.274.273v1.621H3.11v.634h3.875v1.136h-3.2v.634H9.99c-.227.789-.532 1.53-.894 2.202-2.013-.67-4.161-1.212-5.51-.878-.864.214-1.42.597-1.746.998-1.499 1.84-.424 4.633 2.741 4.633 1.872 0 3.675-1.053 5.072-2.787 2.08 1.008 6.37 2.738 6.387 2.745v.105A2.55 2.55 0 0 1 13.5 16H2.541A2.55 2.55 0 0 1 0 13.437V2.563A2.55 2.55 0 0 1 2.541 0" />
<path d="M2.309 9.27c-1.22 1.073-.49 3.034 1.978 3.034 1.434 0 2.868-.925 3.994-2.406-1.602-.789-2.959-1.353-4.425-1.207-.397.04-1.14.217-1.547.58Z" />
</svg>
)
}
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@@ -249,7 +249,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.meta.description":
"Go هو اشتراك بقيمة 10 دولارات شهريًا مع حدود سخية تبلغ 5 ساعات للطلبات لنماذج GLM-5 وKimi K2.5 وMiniMax M2.5.",
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 و MiniMax M2.5.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@@ -258,7 +258,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "اشترك في Go",
"go.cta.price": "$10/شهر",
"go.pricing.body": "استخدمه مع أي وكيل. اشحن الرصيد إذا لزم الأمر. ألغِ في أي وقت.",
"go.cta.promo": "$5 للشهر الأول",
"go.pricing.body":
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية",
"go.graph.go": "Go",
@@ -290,20 +292,20 @@ export const dict = {
"go.testimonials.frank.quote": "أتمنى لو كنت لا أزال في Nvidia.",
"go.problem.title": "ما المشكلة التي يحلها Go؟",
"go.problem.body":
"نحن نركز على جلب تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة (10 دولارات شهريًا) مصمم لجلب البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر.",
"نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة: $5 للشهر الأول، ثم $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.",
"go.problem.subtitle": " ",
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
"go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5",
"go.how.title": "كيف يعمل Go",
"go.how.body": "Go هو اشتراك بقيمة 10 دولارات شهريًا يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا",
"go.how.step1.beforeLink": "اتبع",
"go.how.step1.link": "تعليمات الإعداد",
"go.how.step2.title": "اشترك في Go",
"go.how.step2.link": "$10/شهر",
"go.how.step2.afterLink": "مع حدود سخية",
"go.how.step2.link": "$5 للشهر الأول",
"go.how.step2.afterLink": "ثم $10/شهر مع حدود سخية",
"go.how.step3.title": "ابدأ البرمجة",
"go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر",
"go.privacy.title": "خصوصيتك مهمة بالنسبة لنا",
@@ -319,11 +321,11 @@ export const dict = {
"go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5، مع حدود سخية ووصول موثوق.",
"go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3":
"لا. Zen هو نظام الدفع حسب الاستخدام، بينما Go هو اشتراك بقيمة 10 دولارات شهريًا مع حدود سخية ووصول موثوق لنماذج مفتوحة المصدر GLM-5 وKimi K2.5 وMiniMax M2.5.",
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 و MiniMax M2.5.",
"go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$10/شهر",
"go.faq.a4.p1.afterPricing": "مع حدود سخية.",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
"go.faq.a4.p1.afterPricing": "ثم $10/شهر مع حدود سخية.",
"go.faq.a4.p2.beforeAccount": "يمكنك إدارة اشتراكك في",
"go.faq.a4.p2.accountLink": "حسابك",
"go.faq.a4.p3": "ألغِ في أي وقت.",
@@ -411,12 +413,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "سيتم خصم المبلغ من بطاقتك عند تفعيل اشتراكك",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "الاستخدام",
"workspace.nav.apiKeys": "مفاتيح API",
"workspace.nav.members": "الأعضاء",
"workspace.nav.billing": "الفوترة",
"workspace.nav.settings": "الإعدادات",
"workspace.home.banner.beforeLink": "نماذج محسنة وموثوقة لوكلاء البرمجة.",
"workspace.lite.banner.beforeLink": "نماذج برمجة منخفضة التكلفة للجميع.",
"workspace.home.billing.loading": "جارٍ التحميل...",
"workspace.home.billing.enable": "تمكين الفوترة",
"workspace.home.billing.currentBalance": "الرصيد الحالي",
@@ -535,6 +540,7 @@ export const dict = {
"workspace.billing.loading": "جارٍ التحميل...",
"workspace.billing.addAction": "إضافة",
"workspace.billing.addBalance": "إضافة رصيد",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
"workspace.billing.manage": "إدارة",
"workspace.billing.enable": "تمكين الفوترة",
@@ -616,7 +622,6 @@ export const dict = {
"workspace.lite.time.minute": "دقيقة",
"workspace.lite.time.minutes": "دقائق",
"workspace.lite.time.fewSeconds": "بضع ثوان",
"workspace.lite.subscription.title": "اشتراك Go",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
"workspace.lite.subscription.manage": "إدارة الاشتراك",
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
@@ -626,12 +631,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
"workspace.lite.other.title": "اشتراك Go",
"workspace.lite.black.message":
"أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.",
"workspace.lite.other.message":
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go هو اشتراك بسعر $10 شهريًا يوفر وصولاً موثوقًا إلى نماذج البرمجة المفتوحة الشائعة مع حدود استخدام سخية.",
"يبدأ OpenCode Go بسعر {{price}}، ثم $10/شهر، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.",
"workspace.lite.promo.price": "$5 للشهر الأول",
"workspace.lite.promo.modelsTitle": "ما يتضمنه",
"workspace.lite.promo.footer":
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.",

View File

@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.meta.description":
"O Go é uma assinatura de $10/mês com limites generosos de 5 horas de requisição para GLM-5, Kimi K2.5 e MiniMax M2.5.",
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5, Kimi K2.5 e MiniMax M2.5.",
"go.hero.title": "Modelos de codificação de baixo custo para todos",
"go.hero.body":
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
@@ -262,7 +262,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Assinar o Go",
"go.cta.price": "$10/mês",
"go.pricing.body": "Use com qualquer agente. Recarregue crédito se necessário. Cancele a qualquer momento.",
"go.cta.promo": "$5 no primeiro mês",
"go.pricing.body":
"Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
"go.graph.free": "Grátis",
"go.graph.freePill": "Big Pickle e modelos gratuitos",
"go.graph.go": "Go",
@@ -295,20 +297,21 @@ export const dict = {
"go.testimonials.frank.quote": "Eu queria ainda estar na Nvidia.",
"go.problem.title": "Que problema o Go resolve?",
"go.problem.body":
"Estamos focados em levar a experiência OpenCode para o maior número possível de pessoas. OpenCode Go é uma assinatura de baixo custo ($10/mês) projetada para levar a codificação com agentes para programadores em todo o mundo. Fornece limites generosos e acesso confiável aos modelos de código aberto mais capazes.",
"Estamos focados em levar a experiência do OpenCode para o maior número de pessoas possível. OpenCode Go é uma assinatura de baixo custo: $5 no primeiro mês, depois $10/mês. Oferece limites generosos e acesso confiável aos modelos open source mais capazes.",
"go.problem.subtitle": " ",
"go.problem.item1": "Preço de assinatura de baixo custo",
"go.problem.item2": "Limites generosos e acesso confiável",
"go.problem.item3": "Feito para o maior número possível de programadores",
"go.problem.item4": "Inclui GLM-5, Kimi K2.5 e MiniMax M2.5",
"go.how.title": "Como o Go funciona",
"go.how.body": "Go é uma assinatura de $10/mês que você pode usar com OpenCode ou qualquer agente.",
"go.how.body":
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
"go.how.step1.title": "Crie uma conta",
"go.how.step1.beforeLink": "siga as",
"go.how.step1.link": "instruções de configuração",
"go.how.step2.title": "Assinar o Go",
"go.how.step2.link": "$10/mês",
"go.how.step2.afterLink": "com limites generosos",
"go.how.step2.link": "$5 no primeiro mês",
"go.how.step2.afterLink": "depois $10/mês com limites generosos",
"go.how.step3.title": "Comece a codificar",
"go.how.step3.body": "com acesso confiável a modelos de código aberto",
"go.privacy.title": "Sua privacidade é importante para nós",
@@ -325,11 +328,11 @@ export const dict = {
"go.faq.a2": "Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5, com limites generosos e acesso confiável.",
"go.faq.q3": "O Go é o mesmo que o Zen?",
"go.faq.a3":
"Não. O Zen é pago por uso (pay-as-you-go), enquanto o Go é uma assinatura de $10/mês com limites generosos e acesso confiável aos modelos de código aberto GLM-5, Kimi K2.5 e MiniMax M2.5.",
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5, Kimi K2.5 e MiniMax M2.5.",
"go.faq.q4": "Quanto custa o Go?",
"go.faq.a4.p1.beforePricing": "O Go custa",
"go.faq.a4.p1.pricingLink": "$10/mês",
"go.faq.a4.p1.afterPricing": "com limites generosos.",
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
"go.faq.a4.p1.afterPricing": "depois $10/mês com limites generosos.",
"go.faq.a4.p2.beforeAccount": "Você pode gerenciar sua assinatura em sua",
"go.faq.a4.p2.accountLink": "conta",
"go.faq.a4.p3": "Cancele a qualquer momento.",
@@ -418,12 +421,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Seu cartão será cobrado quando sua assinatura for ativada",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Uso",
"workspace.nav.apiKeys": "Chaves de API",
"workspace.nav.members": "Membros",
"workspace.nav.billing": "Faturamento",
"workspace.nav.settings": "Configurações",
"workspace.home.banner.beforeLink": "Modelos otimizados e confiáveis para agentes de codificação.",
"workspace.lite.banner.beforeLink": "Modelos de codificação de baixo custo para todos.",
"workspace.home.billing.loading": "Carregando...",
"workspace.home.billing.enable": "Ativar faturamento",
"workspace.home.billing.currentBalance": "Saldo atual",
@@ -543,6 +549,7 @@ export const dict = {
"workspace.billing.loading": "Carregando...",
"workspace.billing.addAction": "Adicionar",
"workspace.billing.addBalance": "Adicionar Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Vinculado ao Stripe",
"workspace.billing.manage": "Gerenciar",
"workspace.billing.enable": "Ativar Faturamento",
@@ -625,7 +632,6 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "alguns segundos",
"workspace.lite.subscription.title": "Assinatura Go",
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
@@ -635,12 +641,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
"workspace.lite.other.title": "Assinatura Go",
"workspace.lite.black.message":
"Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.",
"workspace.lite.other.message":
"Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"O OpenCode Go é uma assinatura de $10 por mês que fornece acesso confiável a modelos abertos de codificação populares com limites de uso generosos.",
"O OpenCode Go começa em {{price}}, depois $10/mês, e oferece acesso confiável a modelos de codificação abertos populares com limites de uso generosos.",
"workspace.lite.promo.price": "$5 no primeiro mês",
"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.",

View File

@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.meta.description":
"Go er et abonnement til $10/måned med generøse grænser på 5 timers forespørgsler for GLM-5, Kimi K2.5 og MiniMax M2.5.",
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5, Kimi K2.5 og MiniMax M2.5.",
"go.hero.title": "Kodningsmodeller til lav pris for alle",
"go.hero.body":
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
@@ -260,7 +260,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.pricing.body": "Brug med enhver agent. Genopfyld kredit om nødvendigt. Annuller til enhver tid.",
"go.cta.promo": "$5 første måned",
"go.pricing.body":
"Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go",
@@ -292,20 +294,21 @@ export const dict = {
"go.testimonials.frank.quote": "Jeg ville ønske, jeg stadig var hos Nvidia.",
"go.problem.title": "Hvilket problem løser Go?",
"go.problem.body":
"Vi fokuserer på at bringe OpenCode-oplevelsen til så mange mennesker som muligt. OpenCode Go er et lavprisabonnement ($10/måned) designet til at bringe agentisk kodning til programmører over hele verden. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.",
"Vi fokuserer på at bringe OpenCode-oplevelsen ud til så mange som muligt. OpenCode Go er et lavprisabonnement: $5 for den første måned, derefter $10/måned. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.",
"go.problem.subtitle": " ",
"go.problem.item1": "Lavpris abonnementspriser",
"go.problem.item2": "Generøse grænser og pålidelig adgang",
"go.problem.item3": "Bygget til så mange programmører som muligt",
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5",
"go.how.title": "Hvordan Go virker",
"go.how.body": "Go er et abonnement til $10/måned, som du kan bruge med OpenCode eller enhver anden agent.",
"go.how.body":
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
"go.how.step1.title": "Opret en konto",
"go.how.step1.beforeLink": "følg",
"go.how.step1.link": "opsætningsinstruktionerne",
"go.how.step2.title": "Abonner på Go",
"go.how.step2.link": "$10/måned",
"go.how.step2.afterLink": "med generøse grænser",
"go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "derefter $10/måned med generøse grænser",
"go.how.step3.title": "Start kodning",
"go.how.step3.body": "med pålidelig adgang til open source-modeller",
"go.privacy.title": "Dit privatliv er vigtigt for os",
@@ -322,11 +325,11 @@ export const dict = {
"go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med generøse grænser og pålidelig adgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nej. Zen er pay-as-you-go, mens Go er et abonnement til $10/måned med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5 og MiniMax M2.5.",
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5 og MiniMax M2.5.",
"go.faq.q4": "Hvad koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$10/måned",
"go.faq.a4.p1.afterPricing": "med generøse grænser.",
"go.faq.a4.p1.pricingLink": "$5 første måned",
"go.faq.a4.p1.afterPricing": "derefter $10/måned med generøse grænser.",
"go.faq.a4.p2.beforeAccount": "Du kan administrere dit abonnement i din",
"go.faq.a4.p2.accountLink": "konto",
"go.faq.a4.p3": "Annuller til enhver tid.",
@@ -414,12 +417,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Dit kort vil blive debiteret, når dit abonnement er aktiveret",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Brug",
"workspace.nav.apiKeys": "API-nøgler",
"workspace.nav.members": "Medlemmer",
"workspace.nav.billing": "Fakturering",
"workspace.nav.settings": "Indstillinger",
"workspace.home.banner.beforeLink": "Pålidelige optimerede modeller til kodningsagenter.",
"workspace.lite.banner.beforeLink": "Lavpris kodemodeller for alle.",
"workspace.home.billing.loading": "Indlæser...",
"workspace.home.billing.enable": "Aktiver fakturering",
"workspace.home.billing.currentBalance": "Nuværende saldo",
@@ -539,6 +545,7 @@ export const dict = {
"workspace.billing.loading": "Indlæser...",
"workspace.billing.addAction": "Tilføj",
"workspace.billing.addBalance": "Tilføj saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
"workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",
@@ -621,7 +628,6 @@ export const dict = {
"workspace.lite.time.minute": "minut",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "et par sekunder",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
@@ -631,12 +637,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.black.message":
"Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.",
"workspace.lite.other.message":
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go er et abonnement til $10 om måneden, der giver pålidelig adgang til populære åbne kodningsmodeller med generøse forbrugsgrænser.",
"OpenCode Go starter ved {{price}}, derefter $10/måned, og giver pålidelig adgang til populære åbne kodningsmodeller med generøse brugsgrænser.",
"workspace.lite.promo.price": "$5 for den første måned",
"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.",

View File

@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
"go.meta.description":
"Go ist ein Abonnement für $10/Monat mit großzügigen 5-Stunden-Limits für GLM-5, Kimi K2.5 und MiniMax M2.5.",
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5, Kimi K2.5 und MiniMax M2.5.",
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
"go.hero.body":
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
@@ -262,7 +262,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go abonnieren",
"go.cta.price": "$10/Monat",
"go.pricing.body": "Nutzung mit jedem Agenten. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"go.cta.promo": "$5 im ersten Monat",
"go.pricing.body":
"Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"go.graph.free": "Kostenlos",
"go.graph.freePill": "Big Pickle und kostenlose Modelle",
"go.graph.go": "Go",
@@ -294,20 +296,21 @@ export const dict = {
"go.testimonials.frank.quote": "Ich wünschte, ich wäre noch bei Nvidia.",
"go.problem.title": "Welches Problem löst Go?",
"go.problem.body":
"Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges ($10/Monat) Abonnement, das entwickelt wurde, um Agentic Coding zu Programmierern auf der ganzen Welt zu bringen. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.",
"Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges Abonnement: $5 im ersten Monat, danach $10/Monat. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.",
"go.problem.subtitle": " ",
"go.problem.item1": "Kostengünstiges Abonnement",
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
"go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5",
"go.how.title": "Wie Go funktioniert",
"go.how.body": "Go ist ein Abonnement für $10/Monat, das du mit OpenCode oder jedem anderen Agenten nutzen kannst.",
"go.how.body":
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
"go.how.step1.title": "Konto erstellen",
"go.how.step1.beforeLink": "folge den",
"go.how.step1.link": "Einrichtungsanweisungen",
"go.how.step2.title": "Go abonnieren",
"go.how.step2.link": "$10/Monat",
"go.how.step2.afterLink": "mit großzügigen Limits",
"go.how.step2.link": "$5 im ersten Monat",
"go.how.step2.afterLink": "danach $10/Monat mit großzügigen Limits",
"go.how.step3.title": "Loslegen mit Coding",
"go.how.step3.body": "mit zuverlässigem Zugang zu Open-Source-Modellen",
"go.privacy.title": "Deine Privatsphäre ist uns wichtig",
@@ -324,11 +327,11 @@ export const dict = {
"go.faq.a2": "Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5, mit großzügigen Limits und zuverlässigem Zugang.",
"go.faq.q3": "Ist Go dasselbe wie Zen?",
"go.faq.a3":
"Nein. Zen ist Pay-as-you-go, während Go ein Abonnement für $10/Monat mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5 und MiniMax M2.5 ist.",
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5 und MiniMax M2.5.",
"go.faq.q4": "Wie viel kostet Go?",
"go.faq.a4.p1.beforePricing": "Go kostet",
"go.faq.a4.p1.pricingLink": "$10/Monat",
"go.faq.a4.p1.afterPricing": "mit großzügigen Limits.",
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
"go.faq.a4.p1.afterPricing": "danach $10/Monat mit großzügigen Limits.",
"go.faq.a4.p2.beforeAccount": "Du kannst dein Abonnement in deinem",
"go.faq.a4.p2.accountLink": "Konto verwalten",
"go.faq.a4.p3": "Jederzeit kündbar.",
@@ -417,12 +420,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Deine Karte wird belastet, sobald dein Abonnement aktiviert ist",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Nutzung",
"workspace.nav.apiKeys": "API Keys",
"workspace.nav.members": "Mitglieder",
"workspace.nav.billing": "Abrechnung",
"workspace.nav.settings": "Einstellungen",
"workspace.home.banner.beforeLink": "Zuverlässige, optimierte Modelle für Coding-Agents.",
"workspace.lite.banner.beforeLink": "Kostengünstige Coding-Modelle für alle.",
"workspace.home.billing.loading": "Laden...",
"workspace.home.billing.enable": "Abrechnung aktivieren",
"workspace.home.billing.currentBalance": "Aktuelles Guthaben",
@@ -542,6 +548,7 @@ export const dict = {
"workspace.billing.loading": "Lade...",
"workspace.billing.addAction": "Hinzufügen",
"workspace.billing.addBalance": "Guthaben aufladen",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Mit Stripe verbunden",
"workspace.billing.manage": "Verwalten",
"workspace.billing.enable": "Abrechnung aktivieren",
@@ -624,7 +631,6 @@ export const dict = {
"workspace.lite.time.minute": "Minute",
"workspace.lite.time.minutes": "Minuten",
"workspace.lite.time.fewSeconds": "einige Sekunden",
"workspace.lite.subscription.title": "Go-Abonnement",
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
"workspace.lite.subscription.manage": "Abo verwalten",
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
@@ -634,12 +640,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
"workspace.lite.other.title": "Go-Abonnement",
"workspace.lite.black.message":
"Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.",
"workspace.lite.other.message":
"Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go ist ein Abonnement für $10 pro Monat, das zuverlässigen Zugriff auf beliebte offene Coding-Modelle mit großzügigen Nutzungslimits bietet.",
"OpenCode Go startet bei {{price}}, danach $10/Monat, und bietet zuverlässigen Zugang zu beliebten offenen Coding-Modellen mit großzügigen Nutzungslimits.",
"workspace.lite.promo.price": "$5 im ersten Monat",
"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.",

View File

@@ -21,6 +21,7 @@ export const dict = {
"footer.github": "GitHub",
"footer.docs": "Docs",
"footer.changelog": "Changelog",
"footer.feishu": "Feishu",
"footer.discord": "Discord",
"footer.x": "X",
@@ -247,7 +248,7 @@ export const dict = {
"go.title": "OpenCode Go | Low cost coding models for everyone",
"go.meta.description":
"Go is a $10/month subscription with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
"go.hero.title": "Low cost coding models for everyone",
"go.hero.body":
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
@@ -256,7 +257,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Subscribe to Go",
"go.cta.price": "$10/month",
"go.pricing.body": "Use with any agent. Top up credit if needed. Cancel any time.",
"go.cta.promo": "$5 first month",
"go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.",
"go.graph.free": "Free",
"go.graph.freePill": "Big Pickle and free models",
"go.graph.go": "Go",
@@ -288,20 +290,20 @@ export const dict = {
"go.testimonials.frank.quote": "I wish I was still at Nvidia.",
"go.problem.title": "What problem is Go solving?",
"go.problem.body":
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost ($10/month) subscription designed to bring agentic coding to programmers around the world. It provides generous limits and reliable access to the most capable open source models.",
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost subscription: $5 for your first month, then $10/month. It provides generous limits and reliable access to the most capable open source models.",
"go.problem.subtitle": " ",
"go.problem.item1": "Low cost subscription pricing",
"go.problem.item2": "Generous limits and reliable access",
"go.problem.item3": "Built for as many programmers as possible",
"go.problem.item4": "Includes GLM-5, Kimi K2.5, and MiniMax M2.5",
"go.how.title": "How Go works",
"go.how.body": "Go is a $10/month subscription you can use with OpenCode or any agent.",
"go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.",
"go.how.step1.title": "Create an account",
"go.how.step1.beforeLink": "follow the",
"go.how.step1.link": "setup instructions",
"go.how.step2.title": "Subscribe to Go",
"go.how.step2.link": "$10/month",
"go.how.step2.afterLink": "with generous limits",
"go.how.step2.link": "$5 first month",
"go.how.step2.afterLink": "then $10/month with generous limits",
"go.how.step3.title": "Start coding",
"go.how.step3.body": "with reliable access to open-source models",
"go.privacy.title": "Your privacy is important to us",
@@ -318,11 +320,11 @@ export const dict = {
"go.faq.a2": "Go includes GLM-5, Kimi K2.5, and MiniMax M2.5, with generous limits and reliable access.",
"go.faq.q3": "Is Go the same as Zen?",
"go.faq.a3":
"No. Zen is pay-as-you-go, while Go is a $10/month subscription with generous limits and reliable access to open-source models GLM-5, Kimi K2.5, and MiniMax M2.5.",
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5, Kimi K2.5, and MiniMax M2.5.",
"go.faq.q4": "How much does Go cost?",
"go.faq.a4.p1.beforePricing": "Go costs",
"go.faq.a4.p1.pricingLink": "$10/month",
"go.faq.a4.p1.afterPricing": "with generous limits.",
"go.faq.a4.p1.pricingLink": "$5 first month",
"go.faq.a4.p1.afterPricing": "then $10/month with generous limits.",
"go.faq.a4.p2.beforeAccount": "You can manage your subscription in your",
"go.faq.a4.p2.accountLink": "account",
"go.faq.a4.p3": "Cancel any time.",
@@ -410,12 +412,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Your card will be charged when your subscription is activated",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Usage",
"workspace.nav.apiKeys": "API Keys",
"workspace.nav.members": "Members",
"workspace.nav.billing": "Billing",
"workspace.nav.settings": "Settings",
"workspace.home.banner.beforeLink": "Reliable optimized models for coding agents.",
"workspace.lite.banner.beforeLink": "Low cost coding models for everyone.",
"workspace.home.billing.loading": "Loading...",
"workspace.home.billing.enable": "Enable billing",
"workspace.home.billing.currentBalance": "Current balance",
@@ -535,6 +540,7 @@ export const dict = {
"workspace.billing.loading": "Loading...",
"workspace.billing.addAction": "Add",
"workspace.billing.addBalance": "Add Balance",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Linked to Stripe",
"workspace.billing.manage": "Manage",
"workspace.billing.enable": "Enable Billing",
@@ -617,7 +623,6 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "a few seconds",
"workspace.lite.subscription.title": "Go Subscription",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
"workspace.lite.subscription.manage": "Manage Subscription",
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
@@ -627,12 +632,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.other.title": "Go Subscription",
"workspace.lite.black.message":
"You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.",
"workspace.lite.other.message":
"Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits.",
"OpenCode Go starts at {{price}}, then $10/month, and provides reliable access to popular open coding models with generous usage limits.",
"workspace.lite.promo.price": "$5 for your first month",
"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.",

View File

@@ -254,7 +254,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
"go.meta.description":
"Go es una suscripción de 10 $/mes con generosos límites de solicitudes de 5 horas para GLM-5, Kimi K2.5 y MiniMax M2.5.",
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5, Kimi K2.5 y MiniMax M2.5.",
"go.hero.title": "Modelos de programación de bajo coste para todos",
"go.hero.body":
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
@@ -263,7 +263,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Suscribirse a Go",
"go.cta.price": "10 $/mes",
"go.pricing.body": "Úsalo con cualquier agente. Recarga crédito si es necesario. Cancela en cualquier momento.",
"go.cta.promo": "$5 el primer mes",
"go.pricing.body":
"Úsalo con cualquier agente. $5 el primer mes, luego 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle y modelos gratuitos",
"go.graph.go": "Go",
@@ -296,20 +298,20 @@ export const dict = {
"go.testimonials.frank.quote": "Ojalá siguiera en Nvidia.",
"go.problem.title": "¿Qué problema resuelve Go?",
"go.problem.body":
"Estamos enfocados en llevar la experiencia de OpenCode a tanta gente como sea posible. OpenCode Go es una suscripción de bajo coste (10 $/mes) diseñada para llevar la programación agéntica a programadores de todo el mundo. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.",
"Nos enfocamos en llevar la experiencia de OpenCode a tantas personas como sea posible. OpenCode Go es una suscripción de bajo coste: $5 el primer mes, luego 10 $/mes. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.",
"go.problem.subtitle": " ",
"go.problem.item1": "Precios de suscripción de bajo coste",
"go.problem.item2": "Límites generosos y acceso fiable",
"go.problem.item3": "Creado para tantos programadores como sea posible",
"go.problem.item4": "Incluye GLM-5, Kimi K2.5 y MiniMax M2.5",
"go.how.title": "Cómo funciona Go",
"go.how.body": "Go es una suscripción de 10 $/mes que puedes usar con OpenCode o cualquier agente.",
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
"go.how.step1.title": "Crear una cuenta",
"go.how.step1.beforeLink": "sigue las",
"go.how.step1.link": "instrucciones de configuración",
"go.how.step2.title": "Suscribirse a Go",
"go.how.step2.link": "10 $/mes",
"go.how.step2.afterLink": "con límites generosos",
"go.how.step2.link": "$5 el primer mes",
"go.how.step2.afterLink": "luego 10 $/mes con límites generosos",
"go.how.step3.title": "Empezar a programar",
"go.how.step3.body": "con acceso fiable a modelos de código abierto",
"go.privacy.title": "Tu privacidad es importante para nosotros",
@@ -326,11 +328,11 @@ export const dict = {
"go.faq.a2": "Go incluye GLM-5, Kimi K2.5 y MiniMax M2.5, con límites generosos y acceso fiable.",
"go.faq.q3": "¿Es Go lo mismo que Zen?",
"go.faq.a3":
"No. Zen es pago por uso, mientras que Go es una suscripción de 10 $/mes con límites generosos y acceso fiable a modelos de código abierto GLM-5, Kimi K2.5 y MiniMax M2.5.",
"No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5, Kimi K2.5 y MiniMax M2.5.",
"go.faq.q4": "¿Cuánto cuesta Go?",
"go.faq.a4.p1.beforePricing": "Go cuesta",
"go.faq.a4.p1.pricingLink": "10 $/mes",
"go.faq.a4.p1.afterPricing": "con límites generosos.",
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
"go.faq.a4.p1.afterPricing": "luego 10 $/mes con límites generosos.",
"go.faq.a4.p2.beforeAccount": "Puedes gestionar tu suscripción en tu",
"go.faq.a4.p2.accountLink": "cuenta",
"go.faq.a4.p3": "Cancela en cualquier momento.",
@@ -419,12 +421,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Tu tarjeta se cargará cuando tu suscripción se active",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Uso",
"workspace.nav.apiKeys": "Claves API",
"workspace.nav.members": "Miembros",
"workspace.nav.billing": "Facturación",
"workspace.nav.settings": "Configuración",
"workspace.home.banner.beforeLink": "Modelos optimizados y confiables para agentes de codificación.",
"workspace.lite.banner.beforeLink": "Modelos de codificación de bajo costo para todos.",
"workspace.home.billing.loading": "Cargando...",
"workspace.home.billing.enable": "Habilitar facturación",
"workspace.home.billing.currentBalance": "Saldo actual",
@@ -544,6 +549,7 @@ export const dict = {
"workspace.billing.loading": "Cargando...",
"workspace.billing.addAction": "Añadir",
"workspace.billing.addBalance": "Añadir Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Vinculado con Stripe",
"workspace.billing.manage": "Gestionar",
"workspace.billing.enable": "Habilitar Facturación",
@@ -626,7 +632,6 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minutos",
"workspace.lite.time.fewSeconds": "unos pocos segundos",
"workspace.lite.subscription.title": "Suscripción Go",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestionar Suscripción",
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
@@ -636,12 +641,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
"workspace.lite.other.title": "Suscripción Go",
"workspace.lite.black.message":
"Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.",
"workspace.lite.other.message":
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go es una suscripción de $10 al mes que proporciona acceso confiable a modelos de codificación abiertos populares con generosos límites de uso.",
"OpenCode Go comienza en {{price}}, luego $10/mes, y ofrece acceso confiable a modelos de codificación abiertos populares con límites de uso generosos.",
"workspace.lite.promo.price": "$5 el primer mes",
"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.",

View File

@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
"go.meta.description":
"Go est un abonnement à 10 $/mois avec des limites généreuses de 5 heures de requêtes pour GLM-5, Kimi K2.5 et MiniMax M2.5.",
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5, Kimi K2.5 et MiniMax M2.5.",
"go.hero.title": "Modèles de code à faible coût pour tous",
"go.hero.body":
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
@@ -264,7 +264,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "S'abonner à Go",
"go.cta.price": "10 $/mois",
"go.pricing.body": "Utilisez avec n'importe quel agent. Rechargez du crédit si nécessaire. Annulez à tout moment.",
"go.cta.promo": "$5 le premier mois",
"go.pricing.body":
"Utilisez-le avec n'importe quel agent. $5 le premier mois, puis 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.",
"go.graph.free": "Gratuit",
"go.graph.freePill": "Big Pickle et modèles gratuits",
"go.graph.go": "Go",
@@ -296,20 +298,21 @@ export const dict = {
"go.testimonials.frank.quote": "J'aimerais être encore chez Nvidia.",
"go.problem.title": "Quel problème Go résout-il ?",
"go.problem.body":
"Nous nous concentrons sur le fait d'apporter l'expérience OpenCode à autant de personnes que possible. OpenCode Go est un abonnement à faible coût (10 $/mois) conçu pour apporter le codage agentique aux programmeurs du monde entier. Il offre des limites généreuses et un accès fiable aux modèles open source les plus capables.",
"Nous nous efforçons d'apporter l'expérience OpenCode au plus grand nombre. OpenCode Go est un abonnement à faible coût : $5 pour le premier mois, puis 10 $/mois. Il offre des limites généreuses et un accès fiable aux modèles open source les plus performants.",
"go.problem.subtitle": " ",
"go.problem.item1": "Prix d'abonnement bas",
"go.problem.item2": "Limites généreuses et accès fiable",
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
"go.problem.item4": "Inclut GLM-5, Kimi K2.5 et MiniMax M2.5",
"go.how.title": "Comment fonctionne Go",
"go.how.body": "Go est un abonnement à 10 $/mois que vous pouvez utiliser avec OpenCode ou n'importe quel agent.",
"go.how.body":
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
"go.how.step1.title": "Créez un compte",
"go.how.step1.beforeLink": "suivez les",
"go.how.step1.link": "instructions de configuration",
"go.how.step2.title": "Abonnez-vous à Go",
"go.how.step2.link": "10 $/mois",
"go.how.step2.afterLink": "avec des limites généreuses",
"go.how.step2.link": "$5 le premier mois",
"go.how.step2.afterLink": "puis 10 $/mois avec des limites généreuses",
"go.how.step3.title": "Commencez à coder",
"go.how.step3.body": "avec un accès fiable aux modèles open source",
"go.privacy.title": "Votre vie privée est importante pour nous",
@@ -326,11 +329,11 @@ export const dict = {
"go.faq.a2": "Go inclut GLM-5, Kimi K2.5 et MiniMax M2.5, avec des limites généreuses et un accès fiable.",
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
"go.faq.a3":
"Non. Zen est payé à l'usage (pay-as-you-go), tandis que Go est un abonnement à 10 $/mois avec des limites généreuses et un accès fiable aux modèles open source GLM-5, Kimi K2.5 et MiniMax M2.5.",
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5, Kimi K2.5 et MiniMax M2.5.",
"go.faq.q4": "Combien coûte Go ?",
"go.faq.a4.p1.beforePricing": "Go coûte",
"go.faq.a4.p1.pricingLink": "10 $/mois",
"go.faq.a4.p1.afterPricing": "avec des limites généreuses.",
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
"go.faq.a4.p1.afterPricing": "puis 10 $/mois avec des limites généreuses.",
"go.faq.a4.p2.beforeAccount": "Vous pouvez gérer votre abonnement dans votre",
"go.faq.a4.p2.accountLink": "compte",
"go.faq.a4.p3": "Annulez à tout moment.",
@@ -419,12 +422,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Votre carte sera débitée lorsque votre abonnement sera activé",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Utilisation",
"workspace.nav.apiKeys": "Clés API",
"workspace.nav.members": "Membres",
"workspace.nav.billing": "Facturation",
"workspace.nav.settings": "Paramètres",
"workspace.home.banner.beforeLink": "Modèles optimisés fiables pour les agents de code.",
"workspace.lite.banner.beforeLink": "Modèles de code à faible coût pour tous.",
"workspace.home.billing.loading": "Chargement...",
"workspace.home.billing.enable": "Activer la facturation",
"workspace.home.billing.currentBalance": "Solde actuel",
@@ -545,6 +551,7 @@ export const dict = {
"workspace.billing.loading": "Chargement...",
"workspace.billing.addAction": "Ajouter",
"workspace.billing.addBalance": "Ajouter un solde",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Lié à Stripe",
"workspace.billing.manage": "Gérer",
"workspace.billing.enable": "Activer la facturation",
@@ -630,7 +637,6 @@ export const dict = {
"workspace.lite.time.minute": "minute",
"workspace.lite.time.minutes": "minutes",
"workspace.lite.time.fewSeconds": "quelques secondes",
"workspace.lite.subscription.title": "Abonnement Go",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
"workspace.lite.subscription.manage": "Gérer l'abonnement",
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
@@ -641,12 +647,13 @@ export const dict = {
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider":
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
"workspace.lite.other.title": "Abonnement Go",
"workspace.lite.black.message":
"Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.",
"workspace.lite.other.message":
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go est un abonnement à 10 $ par mois qui offre un accès fiable aux modèles de codage ouverts populaires avec des limites d'utilisation généreuses.",
"OpenCode Go commence à {{price}}, puis 10 $/mois, et offre un accès fiable aux modèles de code ouverts populaires avec des limites d'utilisation généreuses.",
"workspace.lite.promo.price": "$5 le premier mois",
"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.",

View File

@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
"go.meta.description":
"Go è un abbonamento da $10/mese con generosi limiti di richieste di 5 ore per GLM-5, Kimi K2.5 e MiniMax M2.5.",
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5, Kimi K2.5 e MiniMax M2.5.",
"go.hero.title": "Modelli di coding a basso costo per tutti",
"go.hero.body":
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
@@ -260,7 +260,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abbonati a Go",
"go.cta.price": "$10/mese",
"go.pricing.body": "Usa con qualsiasi agente. Ricarica credito se necessario. Annulla in qualsiasi momento.",
"go.cta.promo": "$5 il primo mese",
"go.pricing.body":
"Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle e modelli gratuiti",
"go.graph.go": "Go",
@@ -292,20 +294,20 @@ export const dict = {
"go.testimonials.frank.quote": "Vorrei essere ancora a Nvidia.",
"go.problem.title": "Quale problema risolve Go?",
"go.problem.body":
"Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo ($10/mese) progettato per portare il coding agentico ai programmatori di tutto il mondo. Fornisce limiti generosi e accesso affidabile ai modelli open source più capaci.",
"Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo: $5 il primo mese, poi $10/mese. Offre limiti generosi e accesso affidabile ai modelli open source più capaci.",
"go.problem.subtitle": " ",
"go.problem.item1": "Prezzo di abbonamento a basso costo",
"go.problem.item2": "Limiti generosi e accesso affidabile",
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
"go.problem.item4": "Include GLM-5, Kimi K2.5 e MiniMax M2.5",
"go.how.title": "Come funziona Go",
"go.how.body": "Go è un abbonamento da $10/mese che puoi usare con OpenCode o qualsiasi agente.",
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
"go.how.step1.title": "Crea un account",
"go.how.step1.beforeLink": "segui le",
"go.how.step1.link": "istruzioni di configurazione",
"go.how.step2.title": "Abbonati a Go",
"go.how.step2.link": "$10/mese",
"go.how.step2.afterLink": "con limiti generosi",
"go.how.step2.link": "$5 il primo mese",
"go.how.step2.afterLink": "poi $10/mese con limiti generosi",
"go.how.step3.title": "Inizia a programmare",
"go.how.step3.body": "con accesso affidabile ai modelli open source",
"go.privacy.title": "La tua privacy è importante per noi",
@@ -322,11 +324,11 @@ export const dict = {
"go.faq.a2": "Go include GLM-5, Kimi K2.5 e MiniMax M2.5, con limiti generosi e accesso affidabile.",
"go.faq.q3": "Go è lo stesso di Zen?",
"go.faq.a3":
"No. Zen è a consumo, mentre Go è un abbonamento da $10/mese con limiti generosi e accesso affidabile ai modelli open source GLM-5, Kimi K2.5 e MiniMax M2.5.",
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5, Kimi K2.5 e MiniMax M2.5.",
"go.faq.q4": "Quanto costa Go?",
"go.faq.a4.p1.beforePricing": "Go costa",
"go.faq.a4.p1.pricingLink": "$10/mese",
"go.faq.a4.p1.afterPricing": "con limiti generosi.",
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
"go.faq.a4.p1.afterPricing": "poi $10/mese con limiti generosi.",
"go.faq.a4.p2.beforeAccount": "Puoi gestire il tuo abbonamento nel tuo",
"go.faq.a4.p2.accountLink": "account",
"go.faq.a4.p3": "Annulla in qualsiasi momento.",
@@ -417,12 +419,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "La tua carta verrà addebitata quando il tuo abbonamento sarà attivato",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Utilizzo",
"workspace.nav.apiKeys": "Chiavi API",
"workspace.nav.members": "Membri",
"workspace.nav.billing": "Fatturazione",
"workspace.nav.settings": "Impostazioni",
"workspace.home.banner.beforeLink": "Modelli ottimizzati e affidabili per agenti di coding.",
"workspace.lite.banner.beforeLink": "Modelli di coding a basso costo per tutti.",
"workspace.home.billing.loading": "Caricamento...",
"workspace.home.billing.enable": "Abilita fatturazione",
"workspace.home.billing.currentBalance": "Saldo attuale",
@@ -542,6 +547,7 @@ export const dict = {
"workspace.billing.loading": "Caricamento...",
"workspace.billing.addAction": "Aggiungi",
"workspace.billing.addBalance": "Aggiungi Saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Collegato a Stripe",
"workspace.billing.manage": "Gestisci",
"workspace.billing.enable": "Abilita Fatturazione",
@@ -624,7 +630,6 @@ export const dict = {
"workspace.lite.time.minute": "minuto",
"workspace.lite.time.minutes": "minuti",
"workspace.lite.time.fewSeconds": "pochi secondi",
"workspace.lite.subscription.title": "Abbonamento Go",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
@@ -634,12 +639,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
"workspace.lite.other.title": "Abbonamento Go",
"workspace.lite.black.message":
"Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.",
"workspace.lite.other.message":
"Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go è un abbonamento a $10 al mese che fornisce un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"OpenCode Go parte da {{price}}, poi $10/mese, e offre un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"workspace.lite.promo.price": "$5 il primo mese",
"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.",

View File

@@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.meta.description":
"Goは、GLM-5、Kimi K2.5、MiniMax M2.5を5時間ごとの十分なリクエスト制限で利用できる月額$10のサブスクリプションです。",
"Goは最初の月$5、その後$10/月で、GLM-5、Kimi K2.5、MiniMax M2.5に対して5時間のゆとりあるリクエスト上限があります。",
"go.hero.title": "すべての人のための低価格なコーディングモデル",
"go.hero.body":
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
@@ -259,7 +259,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Goを購読する",
"go.cta.price": "$10/月",
"go.pricing.body": "任意のエージェントで利用可能。必要に応じてクレジットを追加。いつでもキャンセル可能。",
"go.cta.promo": "初月 $5",
"go.pricing.body":
"どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。",
"go.graph.free": "無料",
"go.graph.freePill": "Big Pickleと無料モデル",
"go.graph.go": "Go",
@@ -292,20 +294,20 @@ export const dict = {
"go.testimonials.frank.quote": "まだNvidiaにいられたらよかったのに。",
"go.problem.title": "Goはどのような問題を解決していますか",
"go.problem.body":
"私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは、世界中のプログラマーにエージェント型コーディングをもたらすために設計された低価格($10/月)のサブスクリプションです。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供します。",
"私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは低価格のサブスクリプションで、最初の月は$5、その後は$10/月です。ゆとりある上限と、最も高性能なオープンソースモデルへの信頼できるアクセスを提供します。",
"go.problem.subtitle": " ",
"go.problem.item1": "低価格なサブスクリプション料金",
"go.problem.item2": "十分な制限と安定したアクセス",
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
"go.problem.item4": "GLM-5、Kimi K2.5、MiniMax M2.5を含む",
"go.how.title": "Goの仕組み",
"go.how.body": "GoはOpenCodeまたは任意のエージェントで使用できる月額$10のサブスクリプションです。",
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
"go.how.step1.title": "アカウントを作成",
"go.how.step1.beforeLink": "",
"go.how.step1.link": "セットアップ手順はこちら",
"go.how.step2.title": "Goを購読する",
"go.how.step2.link": "$10/月",
"go.how.step2.afterLink": "(十分な制限付き",
"go.how.step2.link": "最初の月$5",
"go.how.step2.afterLink": "その後$10/月、ゆとりある上限付き",
"go.how.step3.title": "コーディングを開始",
"go.how.step3.body": "オープンソースモデルへの安定したアクセスで",
"go.privacy.title": "あなたのプライバシーは私たちにとって重要です",
@@ -322,11 +324,11 @@ export const dict = {
"go.faq.a2": "Goには、GLM-5、Kimi K2.5、MiniMax M2.5が含まれており、十分な制限と安定したアクセスが提供されます。",
"go.faq.q3": "GoはZenと同じですか",
"go.faq.a3":
"いいえ。Zenは従量課金制ですが、Goは月額$10のサブスクリプションで、GLM-5、Kimi K2.5、MiniMax M2.5といったオープンソースモデルへの十分な制限と安定したアクセスを提供します。",
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5、Kimi K2.5、MiniMax M2.5オープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
"go.faq.q4": "Goの料金は",
"go.faq.a4.p1.beforePricing": "Goは",
"go.faq.a4.p1.pricingLink": "月額$10",
"go.faq.a4.p1.afterPricing": "で、十分な制限が含まれます。",
"go.faq.a4.p1.pricingLink": "最初の月$5",
"go.faq.a4.p1.afterPricing": "その後$10/月、ゆとりある上限付き。",
"go.faq.a4.p2.beforeAccount": "管理画面:",
"go.faq.a4.p2.accountLink": "アカウント",
"go.faq.a4.p3": "いつでもキャンセル可能です。",
@@ -416,12 +418,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "サブスクリプションが有効化された時点でカードに請求されます",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "利用",
"workspace.nav.apiKeys": "APIキー",
"workspace.nav.members": "メンバー",
"workspace.nav.billing": "請求",
"workspace.nav.settings": "設定",
"workspace.home.banner.beforeLink": "コーディングエージェント向けに信頼性の高い最適化されたモデル。",
"workspace.lite.banner.beforeLink": "誰でも使える低コストコーディングモデル。",
"workspace.home.billing.loading": "読み込み中...",
"workspace.home.billing.enable": "課金を有効にする",
"workspace.home.billing.currentBalance": "現在の残高",
@@ -541,6 +546,7 @@ export const dict = {
"workspace.billing.loading": "読み込み中...",
"workspace.billing.addAction": "追加",
"workspace.billing.addBalance": "残高を追加",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Stripeと連携済み",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "課金を有効にする",
@@ -624,7 +630,6 @@ export const dict = {
"workspace.lite.time.minute": "分",
"workspace.lite.time.minutes": "分",
"workspace.lite.time.fewSeconds": "数秒",
"workspace.lite.subscription.title": "Goサブスクリプション",
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
"workspace.lite.subscription.manage": "サブスクリプションの管理",
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
@@ -634,12 +639,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
"workspace.lite.other.title": "Goサブスクリプション",
"workspace.lite.black.message":
"現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。",
"workspace.lite.other.message":
"このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Goは月額$10のサブスクリプションプランで、人気のオープンコーディングモデルへの安定したアクセスを十分な利用枠提供します。",
"OpenCode Goは{{price}}で始まり、その後は$10/月で、人気の高いオープンコーディングモデルへの安定したアクセスと余裕のある利用枠提供します。",
"workspace.lite.promo.price": "初月$5",
"workspace.lite.promo.modelsTitle": "含まれるもの",
"workspace.lite.promo.footer":
"このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。",

View File

@@ -247,7 +247,7 @@ export const dict = {
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.meta.description":
"Go는 GLM-5, Kimi K2.5, MiniMax M2.5에 대해 넉넉한 5시간 요청 한도를 제공하는 월 $10 구독입니다.",
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5에 대해 넉넉한 5시간 요청 한도를 제공니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body":
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
@@ -256,7 +256,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go 구독하기",
"go.cta.price": "$10/월",
"go.pricing.body": "모든 에이전트와 함께 사용하세요. 필요 시 크레딧을 충전하세요. 언제든지 취소 가능.",
"go.cta.promo": "첫 달 $5",
"go.pricing.body":
"어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
"go.graph.free": "무료",
"go.graph.freePill": "Big Pickle 및 무료 모델",
"go.graph.go": "Go",
@@ -289,20 +291,20 @@ export const dict = {
"go.testimonials.frank.quote": "아직 Nvidia에 있었으면 좋았을 텐데요.",
"go.problem.title": "Go는 어떤 문제를 해결하나요?",
"go.problem.body":
"우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공하기 위해 설계된 저렴한(월 $10) 구독입니다. 가장 유능한 오픈 소스 모델에 대해 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 저렴한 구독 서비스로, 첫 달 $5, 이후 $10/월입니다. 넉넉한 한도와 가장 뛰어난 오픈 소스 모델에 대 안정적인 액세스를 제공합니다.",
"go.problem.subtitle": " ",
"go.problem.item1": "저렴한 구독 가격",
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
"go.problem.item4": "GLM-5, Kimi K2.5, MiniMax M2.5 포함",
"go.how.title": "Go 작동 방식",
"go.how.body": "Go는 OpenCode 또는 다른 어떤 에이전트와도 사용할 수 있는 월 $10 구독입니다.",
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있니다.",
"go.how.step1.title": "계정 생성",
"go.how.step1.beforeLink": "",
"go.how.step1.link": "설정 지침을 따르세요",
"go.how.step2.title": "Go 구독",
"go.how.step2.link": "$10/월",
"go.how.step2.afterLink": "(넉넉한 한도 포함)",
"go.how.step2.link": "첫 달 $5",
"go.how.step2.afterLink": "이후 $10/월, 넉넉한 한도 포함",
"go.how.step3.title": "코딩 시작",
"go.how.step3.body": "오픈 소스 모델에 대한 안정적인 액세스와 함께",
"go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다",
@@ -318,11 +320,11 @@ export const dict = {
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5, Kimi K2.5, MiniMax M2.5가 포함됩니다.",
"go.faq.q3": "Go는 Zen과 같은가요?",
"go.faq.a3":
"아니요. Zen은 사용한 만큼 지불(pay-as-you-go)하는 방식인 반면, Go는 월 $10 구독으로 오픈 소스 모델인 GLM-5, Kimi K2.5, MiniMax M2.5에 대 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiniMax M2.5 오픈 소스 모델에 대 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"go.faq.q4": "Go 비용은 얼마인가요?",
"go.faq.a4.p1.beforePricing": "Go 비용은",
"go.faq.a4.p1.pricingLink": "$10/월",
"go.faq.a4.p1.afterPricing": "이며 넉넉한 한도를 제공합니다.",
"go.faq.a4.p1.pricingLink": "첫 달 $5",
"go.faq.a4.p1.afterPricing": "이후 $10/월, 넉넉한 한도 포함.",
"go.faq.a4.p2.beforeAccount": "구독 관리는 다음에서 가능합니다:",
"go.faq.a4.p2.accountLink": "계정",
"go.faq.a4.p3": "언제든지 취소할 수 있습니다.",
@@ -410,12 +412,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "구독이 활성화되면 카드에 청구됩니다",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "사용량",
"workspace.nav.apiKeys": "API 키",
"workspace.nav.members": "멤버",
"workspace.nav.billing": "결제",
"workspace.nav.settings": "설정",
"workspace.home.banner.beforeLink": "코딩 에이전트를 위한 신뢰할 수 있고 최적화된 모델.",
"workspace.lite.banner.beforeLink": "모두를 위한 저비용 코딩 모델.",
"workspace.home.billing.loading": "로드 중...",
"workspace.home.billing.enable": "결제 활성화",
"workspace.home.billing.currentBalance": "현재 잔액",
@@ -535,6 +540,7 @@ export const dict = {
"workspace.billing.loading": "로드 중...",
"workspace.billing.addAction": "추가",
"workspace.billing.addBalance": "잔액 추가",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Stripe에 연결됨",
"workspace.billing.manage": "관리",
"workspace.billing.enable": "결제 활성화",
@@ -616,7 +622,6 @@ export const dict = {
"workspace.lite.time.minute": "분",
"workspace.lite.time.minutes": "분",
"workspace.lite.time.fewSeconds": "몇 초",
"workspace.lite.subscription.title": "Go 구독",
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
"workspace.lite.subscription.manage": "구독 관리",
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
@@ -626,12 +631,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
"workspace.lite.other.title": "Go 구독",
"workspace.lite.black.message":
"현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.",
"workspace.lite.other.message":
"이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go는 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공하는 월 $10의 구독입니다.",
"OpenCode Go는 {{price}}부터 시작하며, 이후 $10/월로 넉넉한 사용 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공니다.",
"workspace.lite.promo.price": "첫 달 $5",
"workspace.lite.promo.modelsTitle": "포함 내역",
"workspace.lite.promo.footer":
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.",

View File

@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
"go.meta.description":
"Go er et abonnement til $10/måned med rause grenser 5 timer for GLM-5, Kimi K2.5 og MiniMax M2.5.",
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5, Kimi K2.5 og MiniMax M2.5.",
"go.hero.title": "Rimelige kodemodeller for alle",
"go.hero.body":
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
@@ -260,7 +260,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.pricing.body": "Bruk med hvilken som helst agent. Fyll på kreditt om nødvendig. Avslutt når som helst.",
"go.cta.promo": "$5 første måned",
"go.pricing.body":
"Bruk med hvilken som helst agent. $5 første måned, deretter $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go",
@@ -292,20 +294,21 @@ export const dict = {
"go.testimonials.frank.quote": "Jeg skulle ønske jeg fortsatt var hos Nvidia.",
"go.problem.title": "Hvilket problem løser Go?",
"go.problem.body":
"Vi fokuserer på å bringe OpenCode-opplevelsen til så mange mennesker som mulig. OpenCode Go er et rimelig ($10/måned) abonnement designet for å bringe agent-koding til programmerere over hele verden. Det gir rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.",
"Vi fokuserer på å bringe OpenCode-opplevelsen til så mange som mulig. OpenCode Go er et rimelig abonnement: $5 for den første måneden, deretter $10/måned. Det gir sjenerøse grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.",
"go.problem.subtitle": " ",
"go.problem.item1": "Rimelig abonnementspris",
"go.problem.item2": "Rause grenser og pålitelig tilgang",
"go.problem.item3": "Bygget for så mange programmerere som mulig",
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5",
"go.how.title": "Hvordan Go fungerer",
"go.how.body": "Go er et abonnement til $10/måned som du kan bruke med OpenCode eller hvilken som helst agent.",
"go.how.body":
"Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
"go.how.step1.title": "Opprett en konto",
"go.how.step1.beforeLink": "følg",
"go.how.step1.link": "oppsettsinstruksjonene",
"go.how.step2.title": "Abonner på Go",
"go.how.step2.link": "$10/måned",
"go.how.step2.afterLink": "med rause grenser",
"go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "deretter $10/måned med sjenerøse grenser",
"go.how.step3.title": "Begynn å kode",
"go.how.step3.body": "med pålitelig tilgang til åpen kildekode-modeller",
"go.privacy.title": "Personvernet ditt er viktig for oss",
@@ -322,11 +325,11 @@ export const dict = {
"go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med rause grenser og pålitelig tilgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nei. Zen er pay-as-you-go, mens Go er et abonnement til $10/måned med rause grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5, Kimi K2.5 og MiniMax M2.5.",
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5, Kimi K2.5 og MiniMax M2.5.",
"go.faq.q4": "Hva koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$10/måned",
"go.faq.a4.p1.afterPricing": "med rause grenser.",
"go.faq.a4.p1.pricingLink": "$5 første måned",
"go.faq.a4.p1.afterPricing": "deretter $10/måned med sjenerøse grenser.",
"go.faq.a4.p2.beforeAccount": "Du kan administrere abonnementet ditt i din",
"go.faq.a4.p2.accountLink": "konto",
"go.faq.a4.p3": "Avslutt når som helst.",
@@ -415,12 +418,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Kortet ditt vil bli belastet når abonnementet aktiveres",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Bruk",
"workspace.nav.apiKeys": "API-nøkler",
"workspace.nav.members": "Medlemmer",
"workspace.nav.billing": "Fakturering",
"workspace.nav.settings": "Innstillinger",
"workspace.home.banner.beforeLink": "Pålitelige optimaliserte modeller for kodeagenter.",
"workspace.lite.banner.beforeLink": "Lavkost kodemodeller for alle.",
"workspace.home.billing.loading": "Laster...",
"workspace.home.billing.enable": "Aktiver fakturering",
"workspace.home.billing.currentBalance": "Gjeldende saldo",
@@ -540,6 +546,7 @@ export const dict = {
"workspace.billing.loading": "Laster...",
"workspace.billing.addAction": "Legg til",
"workspace.billing.addBalance": "Legg til saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Koblet til Stripe",
"workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",
@@ -622,7 +629,6 @@ export const dict = {
"workspace.lite.time.minute": "minutt",
"workspace.lite.time.minutes": "minutter",
"workspace.lite.time.fewSeconds": "noen få sekunder",
"workspace.lite.subscription.title": "Go-abonnement",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
@@ -632,12 +638,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
"workspace.lite.other.title": "Go-abonnement",
"workspace.lite.black.message":
"Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.",
"workspace.lite.other.message":
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go er et abonnement til $10 per måned som gir pålitelig tilgang til populære åpne kodemodeller med rause bruksgrenser.",
"OpenCode Go starter på {{price}}, deretter $10/måned, og gir pålitelig tilgang til populære åpne kodingsmodeller med sjenerøse bruksgrenser.",
"workspace.lite.promo.price": "$5 for den første måneden",
"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.",

View File

@@ -252,7 +252,7 @@ export const dict = {
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
"go.meta.description":
"Go to subskrypcja za $10/miesiąc z hojnymi 5-godzinnymi limitami zapytań dla GLM-5, Kimi K2.5 i MiniMax M2.5.",
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5, Kimi K2.5 i MiniMax M2.5.",
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
"go.hero.body":
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
@@ -261,7 +261,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Zasubskrybuj Go",
"go.cta.price": "$10/miesiąc",
"go.pricing.body": "Używaj z dowolnym agentem. Doładuj środki w razie potrzeby. Anuluj w dowolnym momencie.",
"go.cta.promo": "$5 pierwszy miesiąc",
"go.pricing.body":
"Używaj z dowolnym agentem. $5 za pierwszy miesiąc, potem $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.",
"go.graph.free": "Darmowe",
"go.graph.freePill": "Big Pickle i darmowe modele",
"go.graph.go": "Go",
@@ -293,20 +295,21 @@ export const dict = {
"go.testimonials.frank.quote": "Chciałbym wciąż być w Nvidia.",
"go.problem.title": "Jaki problem rozwiązuje Go?",
"go.problem.body":
"Skupiamy się na dostarczeniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to niskokosztowa ($10/miesiąc) subskrypcja zaprojektowana, aby udostępnić programowanie z agentami programistom na całym świecie. Zapewnia hojne limity i niezawodny dostęp do najzdolniejszych modeli open source.",
"Skupiamy się na udostępnieniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to tania subskrypcja: $5 za pierwszy miesiąc, potem $10/miesiąc. Zapewnia hojne limity i niezawodny dostęp do najbardziej wydajnych modeli open source.",
"go.problem.subtitle": " ",
"go.problem.item1": "Niskokosztowa cena subskrypcji",
"go.problem.item2": "Hojne limity i niezawodny dostęp",
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
"go.problem.item4": "Zawiera GLM-5, Kimi K2.5 i MiniMax M2.5",
"go.how.title": "Jak działa Go",
"go.how.body": "Go to subskrypcja za $10/miesiąc, której możesz używać z OpenCode lub dowolnym agentem.",
"go.how.body":
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
"go.how.step1.title": "Załóż konto",
"go.how.step1.beforeLink": "postępuj zgodnie z",
"go.how.step1.link": "instrukcją konfiguracji",
"go.how.step2.title": "Zasubskrybuj Go",
"go.how.step2.link": "$10/miesiąc",
"go.how.step2.afterLink": "z hojnymi limitami",
"go.how.step2.link": "$5 za pierwszy miesiąc",
"go.how.step2.afterLink": "potem $10/miesiąc z hojnymi limitami",
"go.how.step3.title": "Zacznij kodować",
"go.how.step3.body": "z niezawodnym dostępem do modeli open source",
"go.privacy.title": "Twoja prywatność jest dla nas ważna",
@@ -323,11 +326,11 @@ export const dict = {
"go.faq.a2": "Go zawiera GLM-5, Kimi K2.5 i MiniMax M2.5, z hojnymi limitami i niezawodnym dostępem.",
"go.faq.q3": "Czy Go to to samo co Zen?",
"go.faq.a3":
"Nie. Zen działa w modelu pay-as-you-go (płacisz za użycie), podczas gdy Go to subskrypcja za $10/miesiąc z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5, Kimi K2.5 i MiniMax M2.5.",
"Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5, Kimi K2.5 i MiniMax M2.5.",
"go.faq.q4": "Ile kosztuje Go?",
"go.faq.a4.p1.beforePricing": "Go kosztuje",
"go.faq.a4.p1.pricingLink": "$10/miesiąc",
"go.faq.a4.p1.afterPricing": "z hojnymi limitami.",
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
"go.faq.a4.p1.afterPricing": "potem $10/miesiąc z hojnymi limitami.",
"go.faq.a4.p2.beforeAccount": "Możesz zarządzać subskrypcją na swoim",
"go.faq.a4.p2.accountLink": "koncie",
"go.faq.a4.p3": "Anuluj w dowolnym momencie.",
@@ -416,12 +419,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Twoja karta zostanie obciążona po aktywacji subskrypcji",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Użycie",
"workspace.nav.apiKeys": "Klucze API",
"workspace.nav.members": "Członkowie",
"workspace.nav.billing": "Rozliczenia",
"workspace.nav.settings": "Ustawienia",
"workspace.home.banner.beforeLink": "Niezawodne, zoptymalizowane modele dla agentów kodujących.",
"workspace.lite.banner.beforeLink": "Niskokosztowe modele kodowania dla każdego.",
"workspace.home.billing.loading": "Ładowanie...",
"workspace.home.billing.enable": "Włącz rozliczenia",
"workspace.home.billing.currentBalance": "Aktualne saldo",
@@ -541,6 +547,7 @@ export const dict = {
"workspace.billing.loading": "Ładowanie...",
"workspace.billing.addAction": "Dodaj",
"workspace.billing.addBalance": "Doładuj saldo",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Połączono ze Stripe",
"workspace.billing.manage": "Zarządzaj",
"workspace.billing.enable": "Włącz rozliczenia",
@@ -623,7 +630,6 @@ export const dict = {
"workspace.lite.time.minute": "minuta",
"workspace.lite.time.minutes": "minut(y)",
"workspace.lite.time.fewSeconds": "kilka sekund",
"workspace.lite.subscription.title": "Subskrypcja Go",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
@@ -633,12 +639,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
"workspace.lite.other.title": "Subskrypcja Go",
"workspace.lite.black.message":
"Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.",
"workspace.lite.other.message":
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go to subskrypcja za $10 miesięcznie, która zapewnia niezawodny dostęp do popularnych otwartych modeli do kodowania z hojnymi limitami użycia.",
"OpenCode Go zaczyna się od {{price}}, potem $10/miesiąc, i zapewnia niezawodny dostęp do popularnych otwartych modeli kodowania z hojnymi limitami użycia.",
"workspace.lite.promo.price": "$5 za pierwszy miesiąc",
"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.",

View File

@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.meta.description":
"Go — это подписка за $10/месяц с щедрыми 5-часовыми лимитами запросов для GLM-5, Kimi K2.5 и MiniMax M2.5.",
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5, Kimi K2.5 и MiniMax M2.5.",
"go.hero.title": "Недорогие модели для кодинга для всех",
"go.hero.body":
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
@@ -264,7 +264,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Подписаться на Go",
"go.cta.price": "$10/месяц",
"go.pricing.body": "Используйте с любым агентом. Пополняйте баланс при необходимости. Отменяйте в любое время.",
"go.cta.promo": "$5 первый месяц",
"go.pricing.body":
"Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
"go.graph.free": "Бесплатно",
"go.graph.freePill": "Big Pickle и бесплатные модели",
"go.graph.go": "Go",
@@ -297,20 +299,21 @@ export const dict = {
"go.testimonials.frank.quote": "Жаль, что я больше не в Nvidia.",
"go.problem.title": "Какую проблему решает Go?",
"go.problem.body":
"Мы сосредоточены на том, чтобы сделать OpenCode доступным как можно большему числу людей. OpenCode Go это недорогая ($10/месяц) подписка, разработанная, чтобы сделать агентов-программистов доступными для разработчиков по всему миру. Она предоставляет щедрые лимиты и надежный доступ к самым способным моделям с открытым исходным кодом.",
"Мы стремимся сделать OpenCode доступным для как можно большего числа людей. OpenCode Go - это недорогая подписка: $5 за первый месяц, затем $10/месяц. Она предоставляет щедрые лимиты и надежный доступ к самым мощным моделям с открытым исходным кодом.",
"go.problem.subtitle": " ",
"go.problem.item1": "Недорогая подписка",
"go.problem.item2": "Щедрые лимиты и надежный доступ",
"go.problem.item3": "Создан для максимального числа программистов",
"go.problem.item4": "Включает GLM-5, Kimi K2.5 и MiniMax M2.5",
"go.how.title": "Как работает Go",
"go.how.body": "Go — это подписка за $10/месяц, которую можно использовать с OpenCode или любым агентом.",
"go.how.body":
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
"go.how.step1.title": "Создайте аккаунт",
"go.how.step1.beforeLink": "следуйте",
"go.how.step1.link": "инструкциям по настройке",
"go.how.step2.title": "Подпишитесь на Go",
"go.how.step2.link": "$10/месяц",
"go.how.step2.afterLink": "с щедрыми лимитами",
"go.how.step2.link": "$5 за первый месяц",
"go.how.step2.afterLink": "затем $10/месяц с щедрыми лимитами",
"go.how.step3.title": "Начните кодить",
"go.how.step3.body": "с надежным доступом к open-source моделям",
"go.privacy.title": "Ваша приватность важна для нас",
@@ -327,11 +330,11 @@ export const dict = {
"go.faq.a2": "Go включает GLM-5, Kimi K2.5 и MiniMax M2.5, с щедрыми лимитами и надежным доступом.",
"go.faq.q3": "Go — это то же самое, что и Zen?",
"go.faq.a3":
"Нет. Zen работает по системе оплаты за использование (pay-as-you-go), тогда как Go — это подписка за $10/месяц с щедрыми лимитами и надежным доступом к open-source моделям GLM-5, Kimi K2.5 и MiniMax M2.5.",
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5, Kimi K2.5 и MiniMax M2.5.",
"go.faq.q4": "Сколько стоит Go?",
"go.faq.a4.p1.beforePricing": "Go стоит",
"go.faq.a4.p1.pricingLink": "$10/месяц",
"go.faq.a4.p1.afterPricing": "с щедрыми лимитами.",
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
"go.faq.a4.p1.afterPricing": "затем $10/месяц с щедрыми лимитами.",
"go.faq.a4.p2.beforeAccount": "Вы можете управлять подпиской в своем",
"go.faq.a4.p2.accountLink": "аккаунте",
"go.faq.a4.p3": "Отмена в любое время.",
@@ -421,12 +424,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "С вашей карты будет списана оплата при активации подписки",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Использование",
"workspace.nav.apiKeys": "API Ключи",
"workspace.nav.members": "Участники",
"workspace.nav.billing": "Оплата",
"workspace.nav.settings": "Настройки",
"workspace.home.banner.beforeLink": "Надежные оптимизированные модели для кодинг-агентов.",
"workspace.lite.banner.beforeLink": "Недорогие модели для кодинга, доступные каждому.",
"workspace.home.billing.loading": "Загрузка...",
"workspace.home.billing.enable": "Включить оплату",
"workspace.home.billing.currentBalance": "Текущий баланс",
@@ -547,6 +553,7 @@ export const dict = {
"workspace.billing.loading": "Загрузка...",
"workspace.billing.addAction": "Пополнить",
"workspace.billing.addBalance": "Пополнить баланс",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Привязано к Stripe",
"workspace.billing.manage": "Управление",
"workspace.billing.enable": "Включить оплату",
@@ -629,7 +636,6 @@ export const dict = {
"workspace.lite.time.minute": "минута",
"workspace.lite.time.minutes": "минут",
"workspace.lite.time.fewSeconds": "несколько секунд",
"workspace.lite.subscription.title": "Подписка Go",
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.",
"workspace.lite.subscription.manage": "Управление подпиской",
"workspace.lite.subscription.rollingUsage": "Скользящее использование",
@@ -639,12 +645,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
"workspace.lite.other.title": "Подписка Go",
"workspace.lite.black.message":
"Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.",
"workspace.lite.other.message":
"Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go — это подписка за $10 в месяц, которая предоставляет надежный доступ к популярным открытым моделям для кодинга с щедрыми лимитами использования.",
"OpenCode Go начинается с {{price}}, затем $10/месяц и предоставляет надежный доступ к популярным открытым моделям кодирования с щедрыми лимитами использования.",
"workspace.lite.promo.price": "$5 за первый месяц",
"workspace.lite.promo.modelsTitle": "Что включено",
"workspace.lite.promo.footer":
"План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.",

View File

@@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.meta.description":
"Go คือการสมัครสมาชิกราคา $10/เดือน พร้อมขีดจำกัดการร้องขอที่กว้างขวางถึง 5 ชั่วโมงสำหรับ GLM-5, Kimi K2.5 และ MiniMax M2.5",
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5, Kimi K2.5 และ MiniMax M2.5",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.hero.body":
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
@@ -259,7 +259,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "สมัครสมาชิก Go",
"go.cta.price": "$10/เดือน",
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ เติมเงินเครดิตหากต้องการ ยกเลิกได้ตลอดเวลา",
"go.cta.promo": "$5 เดือนแรก",
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
"go.graph.free": "ฟรี",
"go.graph.freePill": "Big Pickle และโมเดลฟรี",
"go.graph.go": "Go",
@@ -291,20 +292,20 @@ export const dict = {
"go.testimonials.frank.quote": "ผมหวังว่าผมจะยังอยู่ที่ Nvidia",
"go.problem.title": "Go แก้ปัญหาอะไร?",
"go.problem.body":
"เรามุ่งเน้นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด ($10/เดือน) ที่ออกแบบมาเพื่อนำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก โดยมอบขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ",
"เรามุ่งมั่นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด: $5 สำหรับเดือนแรก จากนั้น $10/เดือน โดยมอบขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดอย่างเชื่อถือได้",
"go.problem.subtitle": " ",
"go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ",
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
"go.problem.item4": "รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5",
"go.how.title": "Go ทำงานอย่างไร",
"go.how.body": "Go คือการสมัครสมาชิกราคา $10/เดือน ที่คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.step1.title": "สร้างบัญชี",
"go.how.step1.beforeLink": "ทำตาม",
"go.how.step1.link": "คำแนะนำการตั้งค่า",
"go.how.step2.title": "สมัครสมาชิก Go",
"go.how.step2.link": "$10/เดือน",
"go.how.step2.afterLink": "ด้วยขีดจำกัดที่กว้างขวาง",
"go.how.step2.link": "$5 เดือนแรก",
"go.how.step2.afterLink": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.how.step3.title": "เริ่มเขียนโค้ด",
"go.how.step3.body": "ด้วยการเข้าถึงโมเดลโอเพนซอร์สที่เชื่อถือได้",
"go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา",
@@ -321,11 +322,11 @@ export const dict = {
"go.faq.a2": "Go รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
"go.faq.a3":
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน (pay-as-you-go) ในขณะที่ Go เป็นการสมัครสมาชิกราคา $10/เดือน พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5 และ MiniMax M2.5 ได้อย่างน่าเชื่อถือ",
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5 และ MiniMax M2.5 อย่างเชื่อถือได้",
"go.faq.q4": "Go ราคาเท่าไหร่?",
"go.faq.a4.p1.beforePricing": "Go ราคา",
"go.faq.a4.p1.pricingLink": "$10/เดือน",
"go.faq.a4.p1.afterPricing": "พร้อมขีดจำกัดที่กว้างขวาง",
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
"go.faq.a4.p1.afterPricing": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.faq.a4.p2.beforeAccount": "คุณสามารถจัดการการสมัครสมาชิกของคุณได้ใน",
"go.faq.a4.p2.accountLink": "บัญชีของคุณ",
"go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา",
@@ -413,12 +414,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "บัตรของคุณจะถูกเรียกเก็บเงินเมื่อการสมัครสมาชิกของคุณถูกเปิดใช้งาน",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "การใช้งาน",
"workspace.nav.apiKeys": "API Keys",
"workspace.nav.members": "สมาชิก",
"workspace.nav.billing": "การเรียกเก็บเงิน",
"workspace.nav.settings": "การตั้งค่า",
"workspace.home.banner.beforeLink": "โมเดลที่เชื่อถือได้และปรับแต่งแล้วสำหรับเอเจนต์เขียนโค้ด",
"workspace.lite.banner.beforeLink": "โมเดลเขียนโค้ดต้นทุนต่ำสำหรับทุกคน",
"workspace.home.billing.loading": "กำลังโหลด...",
"workspace.home.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน",
"workspace.home.billing.currentBalance": "ยอดคงเหลือปัจจุบัน",
@@ -538,6 +542,7 @@ export const dict = {
"workspace.billing.loading": "กำลังโหลด...",
"workspace.billing.addAction": "เพิ่ม",
"workspace.billing.addBalance": "เพิ่มยอดคงเหลือ",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "เชื่อมโยงกับ Stripe",
"workspace.billing.manage": "จัดการ",
"workspace.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน",
@@ -620,7 +625,6 @@ export const dict = {
"workspace.lite.time.minute": "นาที",
"workspace.lite.time.minutes": "นาที",
"workspace.lite.time.fewSeconds": "ไม่กี่วินาที",
"workspace.lite.subscription.title": "การสมัครสมาชิก Go",
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว",
"workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก",
"workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน",
@@ -630,12 +634,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
"workspace.lite.other.title": "การสมัครสมาชิก Go",
"workspace.lite.black.message":
"ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go",
"workspace.lite.other.message":
"สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go เป็นการสมัครสมาชิกราคา 10 ดอลลาร์ต่อเดือน ที่ให้การเข้าถึงโมเดลโอเพนโค้ดดิงยอดนิยมได้อย่างเสถียร ด้วยขีดจำกัดการใช้งานที่ครอบคลุม",
"OpenCode Go เริ่มต้นที่ {{price}} จากนั้น $10/เดือน และมอบการเข้าถึงโมเดลการเขียนโค้ดแบบเปิดยอดนิยมอย่างเสถียรพร้อมขีดจำกัดการใช้งานที่ให้มาอย่างเหลือเฟือ",
"workspace.lite.promo.price": "$5 สำหรับเดือนแรก",
"workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย",
"workspace.lite.promo.footer":
"แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ",

View File

@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
"go.meta.description":
"Go, GLM-5, Kimi K2.5 ve MiniMax M2.5 için cömert 5 saatlik istek limitleri sunan aylık 10$'lık bir aboneliktir.",
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5 ve MiniMax M2.5 için cömert 5 saatlik istek limitleri sunar.",
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
"go.hero.body":
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
@@ -262,7 +262,9 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go'ya abone ol",
"go.cta.price": "Ayda 10$",
"go.pricing.body": "Herhangi bir ajanla kullanın. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"go.cta.promo": "İlk ay $5",
"go.pricing.body":
"Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"go.graph.free": "Ücretsiz",
"go.graph.freePill": "Big Pickle ve ücretsiz modeller",
"go.graph.go": "Go",
@@ -295,20 +297,21 @@ export const dict = {
"go.testimonials.frank.quote": "Keşke hala Nvidia'da olsaydım.",
"go.problem.title": "Go hangi sorunu çözüyor?",
"go.problem.body":
"OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklanıyoruz. OpenCode Go, ajan tabanlı kodlamayı dünya çapındaki programcılara sunmak için tasarlanmış düşük maliyetli (ayda 10$) bir aboneliktir. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sağlar.",
"OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklandık. OpenCode Go düşük maliyetli bir aboneliktir: İlk ay $5, sonrasında ayda 10$. Cömert limitler ve en yetenekli açık kaynak modellere güvenilir erişim sağlar.",
"go.problem.subtitle": " ",
"go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması",
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
"go.problem.item4": "GLM-5, Kimi K2.5 ve MiniMax M2.5 içerir",
"go.how.title": "Go nasıl çalışır?",
"go.how.body": "Go, OpenCode veya herhangi bir ajanla kullanabileceğiniz aylık 10$'lık bir aboneliktir.",
"go.how.body":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
"go.how.step1.title": "Bir hesap oluşturun",
"go.how.step1.beforeLink": "takip edin",
"go.how.step1.link": "kurulum talimatları",
"go.how.step2.title": "Go'ya abone olun",
"go.how.step2.link": "Ayda 10$",
"go.how.step2.afterLink": ", cömert limitlerle",
"go.how.step2.link": "İlk ay $5",
"go.how.step2.afterLink": "sonrasında cömert limitlerle ayda 10$",
"go.how.step3.title": "Kodlamaya başlayın",
"go.how.step3.body": "açık kaynaklı modellere güvenilir erişimle",
"go.privacy.title": "Gizliliğiniz bizim için önemlidir",
@@ -325,11 +328,11 @@ export const dict = {
"go.faq.a2": "Go, cömert limitler ve güvenilir erişim ile GLM-5, Kimi K2.5 ve MiniMax M2.5 modellerini içerir.",
"go.faq.q3": "Go, Zen ile aynı mı?",
"go.faq.a3":
"Hayır. Zen kullandıkça öde sistemidir; Go ise GLM-5, Kimi K2.5 ve MiniMax M2.5 açık kaynak modellerine cömert limitler ve güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.",
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5 ve MiniMax M2.5 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
"go.faq.q4": "Go ne kadar?",
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
"go.faq.a4.p1.pricingLink": "ayda 10$",
"go.faq.a4.p1.afterPricing": ", cömert limitlerle.",
"go.faq.a4.p1.pricingLink": "İlk ay $5",
"go.faq.a4.p1.afterPricing": "sonrasında cömert limitlerle ayda 10$.",
"go.faq.a4.p2.beforeAccount": "Aboneliğinizi",
"go.faq.a4.p2.accountLink": "hesabınızdan",
"go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.",
@@ -418,12 +421,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "Aboneliğiniz aktive edildiğinde kartınızdan ödeme alınacaktır",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "Kullanım",
"workspace.nav.apiKeys": "API Anahtarları",
"workspace.nav.members": "Üyeler",
"workspace.nav.billing": "Faturalandırma",
"workspace.nav.settings": "Ayarlar",
"workspace.home.banner.beforeLink": "Kodlama ajanları için güvenilir optimize edilmiş modeller.",
"workspace.lite.banner.beforeLink": "Herkes için düşük maliyetli kodlama modelleri.",
"workspace.home.billing.loading": "Yükleniyor...",
"workspace.home.billing.enable": "Faturalandırmayı etkinleştir",
"workspace.home.billing.currentBalance": "Mevcut bakiye",
@@ -543,6 +549,7 @@ export const dict = {
"workspace.billing.loading": "Yükleniyor...",
"workspace.billing.addAction": "Ekle",
"workspace.billing.addBalance": "Bakiye Ekle",
"workspace.billing.alipay": "Alipay",
"workspace.billing.linkedToStripe": "Stripe'a bağlı",
"workspace.billing.manage": "Yönet",
"workspace.billing.enable": "Faturalandırmayı Etkinleştir",
@@ -625,7 +632,6 @@ export const dict = {
"workspace.lite.time.minute": "dakika",
"workspace.lite.time.minutes": "dakika",
"workspace.lite.time.fewSeconds": "birkaç saniye",
"workspace.lite.subscription.title": "Go Aboneliği",
"workspace.lite.subscription.message": "OpenCode Go abonesisiniz.",
"workspace.lite.subscription.manage": "Aboneliği Yönet",
"workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım",
@@ -635,12 +641,13 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
"workspace.lite.other.title": "Go Aboneliği",
"workspace.lite.black.message":
"Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.",
"workspace.lite.other.message":
"Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go, cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.",
"OpenCode Go {{price}} fiyatından başlar, sonrasında ayda 10$ olur ve cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlar.",
"workspace.lite.promo.price": "İlk ay $5",
"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.",

View File

@@ -24,6 +24,7 @@ export const dict = {
"footer.github": "GitHub",
"footer.docs": "文档",
"footer.changelog": "更新日志",
"footer.feishu": "飞书",
"footer.discord": "Discord",
"footer.x": "X",
@@ -239,7 +240,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情况除外",
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.meta.description": "Go 是每月 $10 的订阅服务,提供对 GLM-5, Kimi K2.5, 和 MiniMax M2.5 的 5 小时充裕请求额。",
"go.meta.description": "Go 月 $5之后 $10/月,提供对 GLM-5Kimi K2.5 和 MiniMax M2.5 的 5 小时充裕请求额。",
"go.hero.title": "人人可用的低成本编程模型",
"go.hero.body":
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
@@ -248,7 +249,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "订阅 Go",
"go.cta.price": "$10/月",
"go.pricing.body": "可配合任何代理使用。按需充值。随时取消。",
"go.cta.promo": "首月 $5",
"go.pricing.body": "可配合任何代理使用。首月 $5之后 $10/月。如有需要可充值。随时取消。",
"go.graph.free": "免费",
"go.graph.freePill": "Big Pickle 和免费模型",
"go.graph.go": "Go",
@@ -280,20 +282,20 @@ export const dict = {
"go.testimonials.frank.quote": "我希望我还在 Nvidia。",
"go.problem.title": "Go 解决了什么问题?",
"go.problem.body":
"我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一低成本 ($10/月) 的订阅服务,旨在将代理编程带给全世界的程序员。它提供充裕的限额和对最强大的开源模型的可靠访问。",
"我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一低成本订阅服务:首月 $5之后 $10/月。它提供充裕的额度,并让您能可靠地使用最强大的开源模型。",
"go.problem.subtitle": " ",
"go.problem.item1": "低成本订阅定价",
"go.problem.item2": "充裕的限额和可靠的访问",
"go.problem.item3": "为尽可能多的程序员打造",
"go.problem.item4": "包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5",
"go.how.title": "Go 如何工作",
"go.how.body": "Go 是每月 $10 的订阅服务,您可以配合 OpenCode 或任何代理使用。",
"go.how.body": "Go 起价为首月 $5之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "创建账户",
"go.how.step1.beforeLink": "遵循",
"go.how.step1.link": "设置说明",
"go.how.step2.title": "订阅 Go",
"go.how.step2.link": "$10/月",
"go.how.step2.afterLink": "享受充裕限额",
"go.how.step2.link": "首月 $5",
"go.how.step2.afterLink": "之后 $10/月,额度充裕",
"go.how.step3.title": "开始编程",
"go.how.step3.body": "可靠访问开源模型",
"go.privacy.title": "您的隐私对我们很重要",
@@ -307,11 +309,11 @@ export const dict = {
"go.faq.a2": "Go 包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5,并提供充裕的限额和可靠的访问。",
"go.faq.q3": "Go 和 Zen 一样吗?",
"go.faq.a3":
"不一样。Zen 是即用即付,而 Go 是每月 $10 的订阅服务,提供对开源模型 GLM-5, Kimi K2.5, 和 MiniMax M2.5 的充裕限额和可靠访问。",
"不。Zen 是按量付费,而 Go 月 $5之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5Kimi K2.5 和 MiniMax M2.5 等开源模型。",
"go.faq.q4": "Go 多少钱?",
"go.faq.a4.p1.beforePricing": "Go 费用为",
"go.faq.a4.p1.pricingLink": "$10/月",
"go.faq.a4.p1.afterPricing": "包含充裕限额。",
"go.faq.a4.p1.pricingLink": "首月 $5",
"go.faq.a4.p1.afterPricing": "之后 $10/月,额度充裕。",
"go.faq.a4.p2.beforeAccount": "您可以在您的",
"go.faq.a4.p2.accountLink": "账户",
"go.faq.a4.p3": "中管理订阅。随时取消。",
@@ -395,12 +397,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "您的卡将在订阅激活时扣费",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "使用量",
"workspace.nav.apiKeys": "API 密钥",
"workspace.nav.members": "成员",
"workspace.nav.billing": "计费",
"workspace.nav.settings": "设置",
"workspace.home.banner.beforeLink": "可靠、优化的编程代理模型。",
"workspace.lite.banner.beforeLink": "低成本编码模型,人人可用。",
"workspace.home.billing.loading": "加载中...",
"workspace.home.billing.enable": "启用计费",
"workspace.home.billing.currentBalance": "当前余额",
@@ -518,6 +523,7 @@ export const dict = {
"workspace.billing.loading": "加载中...",
"workspace.billing.addAction": "充值",
"workspace.billing.addBalance": "充值余额",
"workspace.billing.alipay": "支付宝",
"workspace.billing.linkedToStripe": "已关联 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "启用计费",
@@ -599,7 +605,6 @@ export const dict = {
"workspace.lite.time.minute": "分钟",
"workspace.lite.time.minutes": "分钟",
"workspace.lite.time.fewSeconds": "几秒钟",
"workspace.lite.subscription.title": "Go 订阅",
"workspace.lite.subscription.message": "您已订阅 OpenCode Go。",
"workspace.lite.subscription.manage": "管理订阅",
"workspace.lite.subscription.rollingUsage": "滚动用量",
@@ -609,11 +614,11 @@ export const dict = {
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.lite.subscription.selectProvider":
"在你的 opencode 配置中选择「OpenCode Go」作为提供商即可使用 Go 模型。",
"workspace.lite.other.title": "Go 订阅",
"workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go请先取消订阅",
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go 是一个每月 $10 的订阅计划,提供对主流开源编码模型的稳定访问,并配备充足的使用额。",
"OpenCode Go 起价为 {{price}},之后 $10/月,并提供对流行开放编码模型的可靠访问,同时享有充裕的使用额。",
"workspace.lite.promo.price": "首月 $5",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。",

View File

@@ -24,6 +24,7 @@ export const dict = {
"footer.github": "GitHub",
"footer.docs": "文件",
"footer.changelog": "更新日誌",
"footer.feishu": "飞书",
"footer.discord": "Discord",
"footer.x": "X",
@@ -239,8 +240,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情況",
"go.title": "OpenCode Go | 低成本全民編碼模型",
"go.meta.description":
"Go 是一個每月 $10 的訂閱方案,提供對 GLM-5、Kimi K2.5 與 MiniMax M2.5 的 5 小時寬裕使用限額。",
"go.meta.description": "Go 首月 $5之後 $10/月,提供對 GLM-5、Kimi K2.5 和 MiniMax M2.5 的 5 小時充裕請求額度。",
"go.hero.title": "低成本全民編碼模型",
"go.hero.body":
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
@@ -249,7 +249,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "訂閱 Go",
"go.cta.price": "$10/月",
"go.pricing.body": "可與任何代理一起使用。需要時可儲值額度。隨時取消。",
"go.cta.promo": "首月 $5",
"go.pricing.body": "可搭配任何代理使用。首月 $5之後 $10/月。如有需要可儲值。隨時取消。",
"go.graph.free": "免費",
"go.graph.freePill": "Big Pickle 與免費模型",
"go.graph.go": "Go",
@@ -281,20 +282,20 @@ export const dict = {
"go.testimonials.frank.quote": "我希望我還在 Nvidia。",
"go.problem.title": "Go 正在解決什麼問題?",
"go.problem.body":
"我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一低成本(每月 $10的訂閱方案旨在將代理編碼帶給全世界的程式設計師。它提供裕的限額以及對最強大開源模型的穩定存取。",
"我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一低成本訂閱服務:首月 $5之後 $10/月。它提供裕的額度,並讓您能可靠地使用最強大開源模型。",
"go.problem.subtitle": " ",
"go.problem.item1": "低成本訂閱定價",
"go.problem.item2": "寬裕的限額與穩定存取",
"go.problem.item3": "專為盡可能多的程式設計師打造",
"go.problem.item4": "包含 GLM-5、Kimi K2.5 與 MiniMax M2.5",
"go.how.title": "Go 如何運作",
"go.how.body": "Go 是一個每月 $10 的訂閱方案,你可以將其與 OpenCode 或任何代理一起使用。",
"go.how.body": "Go 起價為首月 $5之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "建立帳號",
"go.how.step1.beforeLink": "遵循",
"go.how.step1.link": "設定說明",
"go.how.step2.title": "訂閱 Go",
"go.how.step2.link": "$10/月",
"go.how.step2.afterLink": "享寬裕限額",
"go.how.step2.link": "首月 $5",
"go.how.step2.afterLink": "之後 $10/月,額度充裕",
"go.how.step3.title": "開始編碼",
"go.how.step3.body": "穩定存取開源模型",
"go.privacy.title": "你的隱私對我們很重要",
@@ -308,11 +309,11 @@ export const dict = {
"go.faq.a2": "Go 包含 GLM-5、Kimi K2.5 與 MiniMax M2.5,並提供寬裕的限額與穩定存取。",
"go.faq.q3": "Go 與 Zen 一樣嗎?",
"go.faq.a3":
"不一樣。Zen 是按量付費,而 Go 是每月 $10 的訂閱方案,提供對開源模型 GLM-5、Kimi K2.5 MiniMax M2.5 的寬裕限額與穩定存取。",
"不。Zen 是按量付費,而 Go 月 $5之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5、Kimi K2.5 MiniMax M2.5 等開源模型。",
"go.faq.q4": "Go 費用是多少?",
"go.faq.a4.p1.beforePricing": "Go 費用為",
"go.faq.a4.p1.pricingLink": "$10/月",
"go.faq.a4.p1.afterPricing": "享寬裕限額。",
"go.faq.a4.p1.pricingLink": "首月 $5",
"go.faq.a4.p1.afterPricing": "之後 $10/月,額度充裕。",
"go.faq.a4.p2.beforeAccount": "你可以在你的",
"go.faq.a4.p2.accountLink": "帳戶",
"go.faq.a4.p3": "中管理訂閱。隨時取消。",
@@ -396,12 +397,15 @@ export const dict = {
"black.subscribe.success.chargeNotice": "你的卡片將在訂閱啟用時扣款",
"workspace.nav.zen": "Zen",
"workspace.nav.go": "Go",
"workspace.nav.usage": "使用量",
"workspace.nav.apiKeys": "API 金鑰",
"workspace.nav.members": "成員",
"workspace.nav.billing": "帳務",
"workspace.nav.settings": "設定",
"workspace.home.banner.beforeLink": "編碼代理的可靠最佳化模型。",
"workspace.lite.banner.beforeLink": "低成本編碼模型,人人可用。",
"workspace.home.billing.loading": "載入中...",
"workspace.home.billing.enable": "啟用帳務",
"workspace.home.billing.currentBalance": "目前餘額",
@@ -519,6 +523,7 @@ export const dict = {
"workspace.billing.loading": "載入中...",
"workspace.billing.addAction": "儲值",
"workspace.billing.addBalance": "儲值餘額",
"workspace.billing.alipay": "支付寶",
"workspace.billing.linkedToStripe": "已連結 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "啟用帳務",
@@ -600,7 +605,6 @@ export const dict = {
"workspace.lite.time.minute": "分鐘",
"workspace.lite.time.minutes": "分鐘",
"workspace.lite.time.fewSeconds": "幾秒",
"workspace.lite.subscription.title": "Go 訂閱",
"workspace.lite.subscription.message": "您已訂閱 OpenCode Go。",
"workspace.lite.subscription.manage": "管理訂閱",
"workspace.lite.subscription.rollingUsage": "滾動使用量",
@@ -610,11 +614,11 @@ export const dict = {
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
"workspace.lite.subscription.selectProvider":
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商即可使用 Go 模型。",
"workspace.lite.other.title": "Go 訂閱",
"workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go請先取消訂閱",
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
"workspace.lite.promo.title": "OpenCode Go",
"workspace.lite.promo.description":
"OpenCode Go 是一個每月 $10 的訂閱方案,提供對主流開放原始碼編碼模型的穩定存取,並配備充足的使用額度。",
"OpenCode Go 起價為 {{price}},之後 $10/月,並提供對熱門開放編碼模型的可靠存取,同時享有充裕的使用額度。",
"workspace.lite.promo.price": "首月 $5",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。",

View File

@@ -0,0 +1,7 @@
import { redirect } from "@solidjs/router"
export async function GET() {
return redirect(
"https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=de8k6664-1b5e-43f2-8efd-21d6772647b5&qr_code=true",
)
}

View File

@@ -368,7 +368,18 @@ body {
text-decoration: none;
[data-slot="cta-price"] {
opacity: 0.6;
display: inline-flex;
align-items: center;
gap: 8px;
}
[data-slot="cta-price-old"] {
opacity: 0.45;
text-decoration: line-through;
}
[data-slot="cta-price-new"] {
opacity: 1;
}
svg {

View File

@@ -62,7 +62,7 @@ function LimitsGraph(props: { href: string }) {
const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
const log = (n: number) => Math.log10(Math.max(n, 1))
const base = 24
const p = 2.2
const p = 1.8
const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
const start = (x(1) / w) * 100
@@ -205,7 +205,7 @@ function LimitsGraph(props: { href: string }) {
export default function Home() {
const workspaceID = createAsync(() => checkLoggedIn())
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/billing` : "/auth"))
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth"))
const i18n = useI18n()
const language = useLanguage()
return (
@@ -320,7 +320,14 @@ export default function Home() {
>
{(part) => {
if (part === "{{text}}") return <span>{i18n.t("go.cta.text")}</span>
if (part === "{{price}}") return <span data-slot="cta-price">{i18n.t("go.cta.price")}</span>
if (part === "{{price}}") {
return (
<span data-slot="cta-price">
<span data-slot="cta-price-old">{i18n.t("go.cta.price")}</span>
<span data-slot="cta-price-new">{i18n.t("go.cta.promo")}</span>
</span>
)
}
return part
}}
</For>

View File

@@ -19,6 +19,12 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
{i18n.t("workspace.nav.zen")}
</A>
<A href={`/workspace/${params.id}/go`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.go")}
</A>
<A href={`/workspace/${params.id}/usage`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.usage")}
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.apiKeys")}
</A>
@@ -41,6 +47,12 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
{i18n.t("workspace.nav.zen")}
</A>
<A href={`/workspace/${params.id}/go`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.go")}
</A>
<A href={`/workspace/${params.id}/usage`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.usage")}
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
{i18n.t("workspace.nav.apiKeys")}
</A>

View File

@@ -3,7 +3,7 @@ import { createMemo, Match, Show, Switch, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { withActor } from "~/context/auth.withActor"
import { IconCreditCard, IconStripe } from "~/component/icon"
import { IconAlipay, IconCreditCard, IconStripe } from "~/component/icon"
import styles from "./billing-section.module.css"
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
import { useI18n } from "~/context/i18n"
@@ -205,6 +205,9 @@ export function BillingSection() {
<Match when={billingInfo()?.paymentMethodType === "link"}>
<IconStripe style={{ width: "24px", height: "24px" }} />
</Match>
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
<IconAlipay style={{ width: "24px", height: "24px" }} />
</Match>
</Switch>
</div>
<div data-slot="card-details">
@@ -218,6 +221,9 @@ export function BillingSection() {
<Match when={billingInfo()?.paymentMethodType === "link"}>
<span data-slot="type">{i18n.t("workspace.billing.linkedToStripe")}</span>
</Match>
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
<span data-slot="type">{i18n.t("workspace.billing.alipay")}</span>
</Match>
</Switch>
</div>
<button

View File

@@ -3,7 +3,6 @@ import { BillingSection } from "./billing-section"
import { ReloadSection } from "./reload-section"
import { PaymentSection } from "./payment-section"
import { BlackSection } from "./black-section"
import { LiteSection } from "./lite-section"
import { createMemo, Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { queryBillingInfo, querySessionInfo } from "../../common"
@@ -21,9 +20,6 @@ export default function () {
<Show when={isBlack()}>
<BlackSection />
</Show>
<Show when={!isBlack()}>
<LiteSection />
</Show>
<BillingSection />
<Show when={billingInfo()?.customerID}>
<ReloadSection />

View File

@@ -0,0 +1,30 @@
import { IconGo } from "~/component/icon"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { LiteSection } from "./lite-section"
export default function () {
const i18n = useI18n()
const language = useLanguage()
return (
<div data-page="workspace-[id]">
<section data-component="header-section">
<IconGo />
<p>
<span>
{i18n.t("workspace.lite.banner.beforeLink")}{" "}
<a target="_blank" href={language.route("/docs/go")}>
{i18n.t("common.learnMore")}
</a>
.
</span>
</p>
</section>
<div data-slot="sections">
<LiteSection />
</div>
</div>
)
}

View File

@@ -167,6 +167,11 @@
color: var(--color-text-secondary);
line-height: 1.5;
margin-top: var(--space-2);
strong {
color: var(--color-text);
font-weight: 600;
}
}
[data-slot="promo-models-title"] {

View File

@@ -1,6 +1,6 @@
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { Show } from "solid-js"
import { createMemo, For, Show } from "solid-js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
@@ -138,6 +138,8 @@ export function LiteSection() {
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked)
const lite = createAsync(() => queryLiteSubscription(params.id!))
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
@@ -166,11 +168,15 @@ export function LiteSection() {
return (
<>
<Show when={lite() && lite()!.mine && lite()!}>
<Show when={isBlack()}>
<section class={styles.root}>
<p data-slot="other-message">{i18n.t("workspace.lite.black.message")}</p>
</section>
</Show>
<Show when={!isBlack() && lite() && lite()!.mine && lite()!}>
{(sub) => (
<section class={styles.root}>
<div data-slot="section-title">
<h2>{i18n.t("workspace.lite.subscription.title")}</h2>
<div data-slot="title-row">
<p>{i18n.t("workspace.lite.subscription.message")}</p>
<button
@@ -248,20 +254,26 @@ export function LiteSection() {
</section>
)}
</Show>
<Show when={lite() && !lite()!.mine}>
<Show when={!isBlack() && lite() && !lite()!.mine}>
<section class={styles.root}>
<div data-slot="section-title">
<h2>{i18n.t("workspace.lite.other.title")}</h2>
</div>
<p data-slot="other-message">{i18n.t("workspace.lite.other.message")}</p>
</section>
</Show>
<Show when={lite() === null}>
<Show when={!isBlack() && lite() === null}>
<section class={styles.root}>
<div data-slot="section-title">
<h2>{i18n.t("workspace.lite.promo.title")}</h2>
</div>
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.description")}</p>
<p data-slot="promo-description">
<For
each={i18n
.t("workspace.lite.promo.description")
.split(/(\{\{price\}\})/g)
.filter(Boolean)}
>
{(part) => {
if (part === "{{price}}") return <strong>{i18n.t("workspace.lite.promo.price")}</strong>
return part
}}
</For>
</p>
<h3 data-slot="promo-models-title">{i18n.t("workspace.lite.promo.modelsTitle")}</h3>
<ul data-slot="promo-models">
<li>Kimi K2.5</li>

View File

@@ -1,12 +1,10 @@
import { Match, Show, Switch, createMemo } from "solid-js"
import { Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
import { NewUserSection } from "./new-user-section"
import { UsageSection } from "./usage-section"
import { ModelSection } from "./model-section"
import { ProviderSection } from "./provider-section"
import { GraphSection } from "./graph-section"
import { IconLogo } from "~/component/icon"
import { IconZen } from "~/component/icon"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
@@ -36,7 +34,7 @@ export default function () {
return (
<div data-page="workspace-[id]">
<section data-component="header-section">
<IconLogo />
<IconZen />
<p>
<span>
{i18n.t("workspace.home.banner.beforeLink")}{" "}
@@ -73,14 +71,10 @@ export default function () {
<div data-slot="sections">
<NewUserSection />
<Show when={userInfo()?.isAdmin}>
<GraphSection />
</Show>
<ModelSection />
<Show when={userInfo()?.isAdmin}>
<ProviderSection />
</Show>
<UsageSection />
</div>
</div>
)

View File

@@ -0,0 +1,21 @@
import { Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { GraphSection } from "./graph-section"
import { UsageSection } from "./usage-section"
import { querySessionInfo } from "../../common"
export default function () {
const params = useParams()
const user = createAsync(() => querySessionInfo(params.id!))
return (
<div data-page="workspace-[id]">
<div data-slot="sections">
<Show when={user()?.isAdmin}>
<GraphSection />
</Show>
<UsageSection />
</div>
</div>
)
}

View File

@@ -1,7 +1,7 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, For, Show, Switch, Match, createEffect, createSignal } from "solid-js"
import { formatDateUTC, formatDateForTable } from "../common"
import { formatDateUTC, formatDateForTable } from "../../common"
import { withActor } from "~/context/auth.withActor"
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
import styles from "./usage-section.module.css"

View File

@@ -24,8 +24,7 @@ import { LocaleLinks } from "~/component/locale-links"
const checkLoggedIn = query(async () => {
"use server"
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
return await getLastSeenWorkspaceID().catch(() => {})
}, "checkLoggedIn.get")
export default function Home() {

View File

@@ -212,13 +212,16 @@ export namespace Billing {
invoice_creation: {
enabled: true,
},
payment_intent_data: {
setup_future_usage: "on_session",
},
payment_method_types: ["card"],
payment_method_data: {
allow_redisplay: "always",
payment_method_options: {
alipay: {},
card: {
setup_future_usage: "on_session",
},
},
payment_method_types: ["card", "alipay"],
//payment_method_data: {
// allow_redisplay: "always",
//},
tax_id_collection: {
enabled: true,
},
@@ -253,6 +256,7 @@ export namespace Billing {
mode: "subscription",
billing_address_collection: "required",
line_items: [{ price: LiteData.priceID(), quantity: 1 }],
discounts: [{ coupon: LiteData.firstMonth50Coupon() }],
...(billing.customerID
? {
customer: billing.customerID,
@@ -265,7 +269,7 @@ export namespace Billing {
customer_email: email!,
}),
currency: "usd",
payment_method_types: ["card"],
payment_method_types: ["card", "alipay"],
tax_id_collection: {
enabled: true,
},

View File

@@ -10,5 +10,6 @@ export namespace LiteData {
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
export const firstMonth50Coupon = fn(z.void(), () => Resource.ZEN_LITE_PRICE.firstMonth50Coupon)
export const planName = fn(z.void(), () => "lite")
}

View File

@@ -131,6 +131,7 @@ declare module "sst" {
"value": string
}
"ZEN_LITE_PRICE": {
"firstMonth50Coupon": string
"price": string
"product": string
"type": "sst.sst.Linkable"

View File

@@ -131,6 +131,7 @@ declare module "sst" {
"value": string
}
"ZEN_LITE_PRICE": {
"firstMonth50Coupon": string
"price": string
"product": string
"type": "sst.sst.Linkable"

View File

@@ -131,6 +131,7 @@ declare module "sst" {
"value": string
}
"ZEN_LITE_PRICE": {
"firstMonth50Coupon": string
"price": string
"product": string
"type": "sst.sst.Linkable"

View File

@@ -27,7 +27,7 @@ export default defineConfig({
},
renderer: {
plugins: [appPlugin],
publicDir: "../app/public",
publicDir: "../../../app/public",
root: "src/renderer",
build: {
rollupOptions: {

View File

@@ -30,6 +30,7 @@
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"effect": "4.0.0-beta.29",
"electron-log": "^5",
"electron-store": "^10",
"electron-updater": "^6",

View File

@@ -78,9 +78,17 @@ async function read(subdir: string, filename: string): Promise<LatestYml | undef
const output: Record<string, string> = {}
// Windows: single arch, pass through
const win = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml")
if (win) output["latest.yml"] = serialize(win)
// Windows: merge arm64 + x64 into single file
const winX64 = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml")
const winArm64 = await read("latest-yml-aarch64-pc-windows-msvc", "latest.yml")
if (winX64 || winArm64) {
const base = winArm64 ?? winX64!
output["latest.yml"] = serialize({
version: base.version,
files: [...(winArm64?.files ?? []), ...(winX64?.files ?? [])],
releaseDate: base.releaseDate,
})
}
// Linux x64: pass through
const linuxX64 = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml")

View File

@@ -19,6 +19,11 @@ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; ass
ocBinary: "opencode-darwin-x64-baseline",
assetExt: "zip",
},
{
rustTarget: "aarch64-pc-windows-msvc",
ocBinary: "opencode-windows-arm64",
assetExt: "zip",
},
{
rustTarget: "x86_64-pc-windows-msvc",
ocBinary: "opencode-windows-x64-baseline",
@@ -41,7 +46,7 @@ export const RUST_TARGET = Bun.env.RUST_TARGET
function nativeTarget() {
const { platform, arch } = process
if (platform === "darwin") return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
if (platform === "win32") return "x86_64-pc-windows-msvc"
if (platform === "win32") return arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"
if (platform === "linux") return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"
throw new Error(`Unsupported platform: ${platform}/${arch}`)
}

View File

@@ -107,7 +107,7 @@ export function syncCli() {
let version = ""
try {
version = execFileSync(installPath, ["--version"]).toString().trim()
version = execFileSync(installPath, ["--version"], { windowsHide: true }).toString().trim()
} catch {
return
}
@@ -147,7 +147,7 @@ export function spawnCommand(args: string, extraEnv: Record<string, string>) {
console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
const child = spawn(cmd, cmdArgs, {
env: envs,
detached: true,
detached: process.platform !== "win32",
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
})

View File

@@ -31,35 +31,13 @@ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigratio
import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import {
checkHealth,
checkHealthOrAskRetry,
getDefaultServerUrl,
getSavedServerUrl,
getWslConfig,
setDefaultServerUrl,
setWslConfig,
spawnLocalServer,
} from "./server"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows"
type ServerConnection =
| { variant: "existing"; url: string }
| {
variant: "cli"
url: string
password: null | string
health: {
wait: Promise<void>
}
events: any
}
const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" }
let mainWindow: BrowserWindow | null = null
const loadingWindow: BrowserWindow | null = null
let sidecar: CommandChild | null = null
const loadingComplete = defer<void>()
@@ -131,77 +109,48 @@ function setInitStep(step: InitStep) {
initEmitter.emit("step", step)
}
async function setupServerConnection(): Promise<ServerConnection> {
const customUrl = await getSavedServerUrl()
if (customUrl && (await checkHealthOrAskRetry(customUrl))) {
serverReady.resolve({ url: customUrl, password: null })
return { variant: "existing", url: customUrl }
}
const port = await getSidecarPort()
const hostname = "127.0.0.1"
const localUrl = `http://${hostname}:${port}`
if (await checkHealth(localUrl)) {
serverReady.resolve({ url: localUrl, password: null })
return { variant: "existing", url: localUrl }
}
const password = randomUUID()
const { child, health, events } = spawnLocalServer(hostname, port, password)
sidecar = child
return {
variant: "cli",
url: localUrl,
password,
health,
events,
}
}
async function initialize() {
const needsMigration = !sqliteFileExists()
const sqliteDone = needsMigration ? defer<void>() : undefined
let overlay: BrowserWindow | null = null
const port = await getSidecarPort()
const hostname = "127.0.0.1"
const url = `http://${hostname}:${port}`
const password = randomUUID()
logger.log("spawning sidecar", { url })
const { child, health, events } = spawnLocalServer(hostname, port, password)
sidecar = child
serverReady.resolve({
url,
username: "opencode",
password,
})
const loadingTask = (async () => {
logger.log("setting up server connection")
const serverConnection = await setupServerConnection()
logger.log("server connection ready", {
variant: serverConnection.variant,
url: serverConnection.url,
logger.log("sidecar connection started", { url })
events.on("sqlite", (progress: SqliteMigrationProgress) => {
setInitStep({ phase: "sqlite_waiting" })
if (overlay) sendSqliteMigrationProgress(overlay, progress)
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
if (progress.type === "Done") sqliteDone?.resolve()
})
const cliHealthCheck = (() => {
if (serverConnection.variant == "cli") {
return async () => {
const { events, health } = serverConnection
events.on("sqlite", (progress: SqliteMigrationProgress) => {
setInitStep({ phase: "sqlite_waiting" })
if (loadingWindow) sendSqliteMigrationProgress(loadingWindow, progress)
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
if (progress.type === "Done") sqliteDone?.resolve()
})
await health.wait
serverReady.resolve({
url: serverConnection.url,
password: serverConnection.password,
})
}
} else {
serverReady.resolve({ url: serverConnection.url, password: null })
return null
}
})()
logger.log("server connection started")
if (cliHealthCheck) {
if (needsMigration) await sqliteDone?.promise
cliHealthCheck?.()
if (needsMigration) {
await sqliteDone?.promise
}
await Promise.race([
health.wait,
delay(30_000).then(() => {
throw new Error("Sidecar health check timed out")
}),
]).catch((error) => {
logger.error("sidecar health check failed", error)
})
logger.log("loading task finished")
})()
@@ -211,32 +160,26 @@ async function initialize() {
deepLinks: pendingDeepLinks,
}
const loadingWindow = await (async () => {
if (needsMigration /** TOOD: 1 second timeout */) {
// showLoading = await Promise.race([init.then(() => false).catch(() => false), delay(1000).then(() => true)])
const loadingWindow = createLoadingWindow(globals)
await delay(1000)
return loadingWindow
} else {
logger.log("showing main window without loading window")
mainWindow = createMainWindow(globals)
wireMenu()
wireMenu()
if (needsMigration) {
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
if (show) {
overlay = createLoadingWindow(globals)
await delay(1_000)
}
})()
}
await loadingTask
setInitStep({ phase: "done" })
if (loadingWindow) {
if (overlay) {
await loadingComplete.promise
}
if (!mainWindow) {
mainWindow = createMainWindow(globals)
wireMenu()
}
mainWindow = createMainWindow(globals)
loadingWindow?.close()
overlay?.close()
}
function wireMenu() {

View File

@@ -2,8 +2,9 @@ import { execFile } from "node:child_process"
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
import { getStore } from "./store"
import { setTitlebar } from "./windows"
type Deps = {
killSidecar: () => void
@@ -161,6 +162,11 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (!win) return
setTitlebar(win, theme)
})
}
export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {

View File

@@ -1,6 +1,4 @@
import { dialog } from "electron"
import { getConfig, serve, type CommandChild, type Config } from "./cli"
import { serve, type CommandChild } from "./cli"
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
import { store } from "./store"
@@ -31,15 +29,6 @@ export function setWslConfig(config: WslConfig) {
store.set(WSL_ENABLED_KEY, config.enabled)
}
export async function getSavedServerUrl(): Promise<string | null> {
const direct = getDefaultServerUrl()
if (direct) return direct
const config = await getConfig().catch(() => null)
if (!config) return null
return getServerUrlFromConfig(config)
}
export function spawnLocalServer(hostname: string, port: number, password: string) {
const { child, exit, events } = serve(hostname, port, password)
@@ -94,36 +83,4 @@ export async function checkHealth(url: string, password?: string | null): Promis
}
}
export async function checkHealthOrAskRetry(url: string): Promise<boolean> {
while (true) {
if (await checkHealth(url)) return true
const result = await dialog.showMessageBox({
type: "warning",
message: `Could not connect to configured server:\n${url}\n\nWould you like to retry or start a local server instead?`,
title: "Connection Failed",
buttons: ["Retry", "Start Local"],
defaultId: 0,
cancelId: 1,
})
if (result.response === 0) continue
return false
}
}
export function normalizeHostnameForUrl(hostname: string) {
if (hostname === "0.0.0.0") return "127.0.0.1"
if (hostname === "::") return "[::1]"
if (hostname.includes(":") && !hostname.startsWith("[")) return `[${hostname}]`
return hostname
}
export function getServerUrlFromConfig(config: Config) {
const server = config.server
if (!server?.port) return null
const host = server.hostname ? normalizeHostnameForUrl(server.hostname) : "127.0.0.1"
return `http://${host}:${server.port}`
}
export type { CommandChild }

View File

@@ -1,7 +1,8 @@
import windowState from "electron-window-state"
import { app, BrowserWindow, nativeImage } from "electron"
import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import type { TitlebarTheme } from "../preload/types"
type Globals = {
updaterEnabled: boolean
@@ -20,6 +21,24 @@ function iconPath() {
return join(iconsDir(), `icon.${ext}`)
}
function tone() {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
}
function overlay(theme: Partial<TitlebarTheme> = {}) {
const mode = theme.mode ?? tone()
return {
color: "#00000000",
symbolColor: mode === "dark" ? "white" : "black",
height: 40,
}
}
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
if (process.platform !== "win32") return
win.setTitleBarOverlay(overlay(theme))
}
export function setDockIcon() {
if (process.platform !== "darwin") return
app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "128x128@2x.png")))
@@ -31,6 +50,7 @@ export function createMainWindow(globals: Globals) {
defaultHeight: 800,
})
const mode = tone()
const win = new BrowserWindow({
x: state.x,
y: state.y,
@@ -49,11 +69,7 @@ export function createMainWindow(globals: Globals) {
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: {
color: "transparent",
symbolColor: "#999",
height: 40,
},
titleBarOverlay: overlay({ mode }),
}
: {}),
webPreferences: {
@@ -71,6 +87,7 @@ export function createMainWindow(globals: Globals) {
}
export function createLoadingWindow(globals: Globals) {
const mode = tone()
const win = new BrowserWindow({
width: 640,
height: 480,
@@ -83,11 +100,7 @@ export function createLoadingWindow(globals: Globals) {
? {
frame: false,
titleBarStyle: "hidden" as const,
titleBarOverlay: {
color: "transparent",
symbolColor: "#999",
height: 40,
},
titleBarOverlay: overlay({ mode }),
}
: {}),
webPreferences: {

View File

@@ -57,6 +57,7 @@ const api: ElectronAPI = {
relaunch: () => ipcRenderer.send("relaunch"),
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
checkUpdate: () => ipcRenderer.invoke("check-update"),

View File

@@ -2,6 +2,7 @@ export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" }
export type ServerReadyData = {
url: string
username: string | null
password: string | null
}
@@ -10,6 +11,9 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | {
export type WslConfig = { enabled: boolean }
export type LinuxDisplayBackend = "wayland" | "auto"
export type TitlebarTheme = {
mode: "light" | "dark"
}
export type ElectronAPI = {
killSidecar: () => Promise<void>
@@ -57,6 +61,7 @@ export type ElectronAPI = {
relaunch: () => void
getZoomFactor: () => Promise<number>
setZoomFactor: (factor: number) => Promise<void>
setTitlebar: (theme: TitlebarTheme) => Promise<void>
loadingWindowComplete: () => void
runUpdater: (alertOnFail: boolean) => Promise<void>
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>

View File

@@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test"
import { join, dirname, resolve } from "node:path"
import { existsSync } from "node:fs"
import { fileURLToPath } from "node:url"
const dir = dirname(fileURLToPath(import.meta.url))
const root = resolve(dir, "../..")
const html = async (name: string) => Bun.file(join(dir, name)).text()
/**
* Electron loads renderer HTML via `win.loadFile()` which uses the `file://`
* protocol. Absolute paths like `src="/foo.js"` resolve to the filesystem root
* (e.g. `file:///C:/foo.js` on Windows) instead of relative to the app bundle.
*
* All local resource references must use relative paths (`./`).
*/
describe("electron renderer html", () => {
for (const name of ["index.html", "loading.html"]) {
describe(name, () => {
test("script src attributes use relative paths", async () => {
const content = await html(name)
const srcs = [...content.matchAll(/\bsrc=["']([^"']+)["']/g)].map((m) => m[1])
for (const src of srcs) {
expect(src).not.toMatch(/^\/[^/]/)
}
})
test("link href attributes use relative paths", async () => {
const content = await html(name)
const hrefs = [...content.matchAll(/<link[^>]+href=["']([^"']+)["']/g)].map((m) => m[1])
for (const href of hrefs) {
expect(href).not.toMatch(/^\/[^/]/)
}
})
test("no web manifest link (not applicable in Electron)", async () => {
const content = await html(name)
expect(content).not.toContain('rel="manifest"')
})
})
}
})
/**
* Vite resolves `publicDir` relative to `root`, not the config file.
* This test reads the actual values from electron.vite.config.ts to catch
* regressions where the publicDir path no longer resolves correctly
* after the renderer root is accounted for.
*/
describe("electron vite publicDir", () => {
test("configured publicDir resolves to a directory with oc-theme-preload.js", async () => {
const config = await Bun.file(join(root, "electron.vite.config.ts")).text()
const pub = config.match(/publicDir:\s*["']([^"']+)["']/)
const rendererRoot = config.match(/root:\s*["']([^"']+)["']/)
expect(pub).not.toBeNull()
expect(rendererRoot).not.toBeNull()
const resolved = resolve(root, rendererRoot![1], pub![1])
expect(existsSync(resolved)).toBe(true)
expect(existsSync(join(resolved, "oc-theme-preload.js"))).toBe(true)
})
})

View File

@@ -4,20 +4,19 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenCode</title>
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
<link rel="shortcut icon" href="/favicon-v3.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/png" href="./favicon-96x96-v3.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="./favicon-v3.svg" />
<link rel="shortcut icon" href="./favicon-v3.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
<meta name="theme-color" content="#F8F7F7" />
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
<meta property="og:image" content="./social-share.png" />
<meta property="twitter:image" content="./social-share.png" />
<script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh"></div>
<script src="/index.tsx" type="module"></script>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -9,9 +9,8 @@ import {
ServerConnection,
useCommand,
} from "@opencode-ai/app"
import { Splash } from "@opencode-ai/ui/logo"
import type { AsyncStorage } from "@solid-primitives/storage"
import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
import { createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import { MemoryRouter } from "@solidjs/router"
import pkg from "../../package.json"
@@ -19,7 +18,6 @@ import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
import type { ServerReadyData } from "../preload/types"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -198,11 +196,13 @@ const createPlatform = (): Platform => {
await window.api.setWslConfig({ enabled })
},
getDefaultServerUrl: async () => {
return window.api.getDefaultServerUrl().catch(() => null)
getDefaultServer: async () => {
const url = await window.api.getDefaultServerUrl().catch(() => null)
if (!url) return null
return ServerConnection.Key.make(url)
},
setDefaultServerUrl: async (url: string | null) => {
setDefaultServer: async (url: string | null) => {
await window.api.setDefaultServerUrl(url)
},
@@ -240,6 +240,31 @@ listenForDeepLinks()
render(() => {
const platform = createPlatform()
// Fetch sidecar credentials (available immediately, before health check)
const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined))
const [defaultServer] = createResource(() =>
platform.getDefaultServer?.().then((url) => {
if (url) return ServerConnection.key({ type: "http", http: { url } })
}),
)
const servers = () => {
const data = sidecar()
if (!data) return []
const server: ServerConnection.Sidecar = {
displayName: "Local Server",
type: "sidecar",
variant: "base",
http: {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
},
}
return [server] as ServerConnection.Any[]
}
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
@@ -248,6 +273,12 @@ render(() => {
}
}
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
return null
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
@@ -258,55 +289,20 @@ render(() => {
return (
<PlatformProvider value={platform}>
<AppBaseProviders>
<ServerGate>
{(data) => {
const server: ServerConnection.Sidecar = {
displayName: "Local Server",
type: "sidecar",
variant: "base",
http: {
url: data().url,
username: "opencode",
password: data().password ?? undefined,
},
}
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
return null
}
<Show when={!defaultServer.loading && !sidecar.loading}>
{(_) => {
return (
<AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} router={MemoryRouter}>
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()}
router={MemoryRouter}
>
<Inner />
</AppInterface>
)
}}
</ServerGate>
</Show>
</AppBaseProviders>
</PlatformProvider>
)
}, root!)
// Gate component that waits for the server to be ready
function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
const [serverData] = createResource(() => window.api.awaitInitialization(() => undefined))
console.log({ serverData })
if (serverData.state === "errored") throw serverData.error
return (
<Show
when={serverData.state !== "pending" && serverData()}
fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
</div>
}
>
{(data) => props.children(data)}
</Show>
)
}

View File

@@ -4,20 +4,19 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenCode</title>
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />
<link rel="shortcut icon" href="/favicon-v3.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" type="image/png" href="./favicon-96x96-v3.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="./favicon-v3.svg" />
<link rel="shortcut icon" href="./favicon-v3.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
<meta name="theme-color" content="#F8F7F7" />
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
<meta property="og:image" content="./social-share.png" />
<meta property="twitter:image" content="./social-share.png" />
<script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh"></div>
<script src="/loading.tsx" type="module"></script>
<script src="./loading.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
import { render } from "solid-js/web"
import { MetaProvider } from "@solidjs/meta"
import { render } from "solid-js/web"
import "@opencode-ai/app/index.css"
import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
@@ -34,7 +34,10 @@ render(() => {
const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => {
if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
if (progress.type === "Done") setPercent(100)
if (progress.type === "Done") {
setPercent(100)
setStep({ phase: "done" })
}
})
onCleanup(() => {

View File

@@ -18,5 +18,6 @@
"types": ["vite/client", "node", "electron"]
},
"references": [{ "path": "../app" }],
"include": ["src", "package.json"]
"include": ["src", "package.json"],
"exclude": ["src/**/*.test.ts"]
}

View File

@@ -101,6 +101,7 @@ const targets = [
{ key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" },
{ key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" },
{ key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" },
{ key: "windows-aarch64-nsis", asset: "opencode-desktop-windows-arm64.exe" },
{ key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" },
{ key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" },
{
@@ -129,6 +130,7 @@ const alias = (key: string, source: string) => {
alias("linux-x86_64", "linux-x86_64-deb")
alias("linux-aarch64", "linux-aarch64-deb")
alias("windows-aarch64", "windows-aarch64-nsis")
alias("windows-x86_64", "windows-x86_64-nsis")
alias("darwin-x86_64", "darwin-x86_64-app")
alias("darwin-aarch64", "darwin-aarch64-app")

View File

@@ -11,6 +11,11 @@ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; ass
ocBinary: "opencode-darwin-x64-baseline",
assetExt: "zip",
},
{
rustTarget: "aarch64-pc-windows-msvc",
ocBinary: "opencode-windows-arm64",
assetExt: "zip",
},
{
rustTarget: "x86_64-pc-windows-msvc",
ocBinary: "opencode-windows-x64-baseline",

View File

@@ -12,12 +12,10 @@ mod window_customizer;
mod windows;
use crate::cli::CommandChild;
use futures::{
FutureExt, TryFutureExt,
future::{self, Shared},
};
use futures::{FutureExt, TryFutureExt};
use std::{
env,
future::Future,
net::TcpListener,
path::PathBuf,
process::Command,
@@ -35,7 +33,6 @@ use tokio::{
use crate::cli::{sqlite_migration::SqliteMigrationProgress, sync_cli};
use crate::constants::*;
use crate::server::get_saved_server_url;
use crate::windows::{LoadingWindow, MainWindow};
#[derive(Clone, serde::Serialize, specta::Type, Debug)]
@@ -43,7 +40,6 @@ struct ServerReadyData {
url: String,
username: Option<String>,
password: Option<String>,
is_sidecar: bool,
}
#[derive(Clone, Copy, serde::Serialize, specta::Type, Debug)]
@@ -65,27 +61,12 @@ struct InitState {
current: watch::Receiver<InitStep>,
}
#[derive(Clone)]
struct ServerState {
child: Arc<Mutex<Option<CommandChild>>>,
status: future::Shared<oneshot::Receiver<Result<ServerReadyData, String>>>,
}
impl ServerState {
pub fn new(
child: Option<CommandChild>,
status: Shared<oneshot::Receiver<Result<ServerReadyData, String>>>,
) -> Self {
Self {
child: Arc::new(Mutex::new(child)),
status,
}
}
pub fn set_child(&self, child: Option<CommandChild>) {
*self.child.lock().unwrap() = child;
}
}
/// Resolves with sidecar credentials as soon as the sidecar is spawned (before health check).
struct SidecarReady(futures::future::Shared<oneshot::Receiver<ServerReadyData>>);
#[tauri::command]
#[specta::specta]
@@ -110,26 +91,21 @@ fn kill_sidecar(app: AppHandle) {
tracing::info!("Killed server");
}
fn get_logs() -> String {
logging::tail()
}
#[tauri::command]
#[specta::specta]
async fn await_initialization(
state: State<'_, ServerState>,
state: State<'_, SidecarReady>,
init_state: State<'_, InitState>,
events: Channel<InitStep>,
) -> Result<ServerReadyData, String> {
let mut rx = init_state.current.clone();
let events = async {
let stream = async {
let e = *rx.borrow();
let _ = events.send(e);
while rx.changed().await.is_ok() {
let step = *rx.borrow_and_update();
let _ = events.send(step);
if matches!(step, InitStep::Done) {
@@ -138,10 +114,18 @@ async fn await_initialization(
}
};
future::join(state.status.clone(), events)
.await
.0
.map_err(|_| "Failed to get server status".to_string())?
// Wait for sidecar credentials (available immediately after spawn, before health check)
let data = async {
state
.inner()
.0
.clone()
.await
.map_err(|_| "Failed to get sidecar data".to_string())
};
let (result, _) = futures::future::join(data, stream).await;
result
}
#[tauri::command]
@@ -439,22 +423,35 @@ async fn initialize(app: AppHandle) {
setup_app(&app, init_rx);
spawn_cli_sync_task(app.clone());
let (server_ready_tx, server_ready_rx) = oneshot::channel();
let server_ready_rx = server_ready_rx.shared();
app.manage(ServerState::new(None, server_ready_rx.clone()));
// Spawn sidecar immediately - credentials are known before health check
let port = get_sidecar_port();
let hostname = "127.0.0.1";
let url = format!("http://{hostname}:{port}");
let password = uuid::Uuid::new_v4().to_string();
tracing::info!("Spawning sidecar on {url}");
let (child, health_check) =
server::spawn_local_server(app.clone(), hostname.to_string(), port, password.clone());
// Make sidecar credentials available immediately (before health check completes)
let (ready_tx, ready_rx) = oneshot::channel();
let _ = ready_tx.send(ServerReadyData {
url: url.clone(),
username: Some("opencode".to_string()),
password: Some(password),
});
app.manage(SidecarReady(ready_rx.shared()));
app.manage(ServerState {
child: Arc::new(Mutex::new(Some(child))),
});
let loading_window_complete = event_once_fut::<LoadingWindowComplete>(&app);
tracing::info!("Main and loading windows created");
// SQLite migration handling:
// We only do this if the sqlite db doesn't exist, and we're expecting the sidecar to create it
// First, we spawn a task that listens for SqliteMigrationProgress events that can
// come from any invocation of the sidecar CLI. The progress is captured by a stdout stream interceptor.
// Then in the loading task, we wait for sqlite migration to complete before
// starting our health check against the server, otherwise long migrations could result in a timeout.
let needs_sqlite_migration = !sqlite_file_exists();
let sqlite_done = needs_sqlite_migration.then(|| {
// We only do this if the sqlite db doesn't exist, and we're expecting the sidecar to create it.
// A separate loading window is shown for long migrations.
let needs_migration = !sqlite_file_exists();
let sqlite_done = needs_migration.then(|| {
tracing::info!(
path = %opencode_db_path().expect("failed to get db path").display(),
"Sqlite file not found, waiting for it to be generated"
@@ -480,80 +477,22 @@ async fn initialize(app: AppHandle) {
}))
});
// The loading task waits for SQLite migration (if needed) then for the sidecar health check.
// This is only used to drive the loading window progress - the main window is shown immediately.
let loading_task = tokio::spawn({
let app = app.clone();
async move {
tracing::info!("Setting up server connection");
let server_connection = setup_server_connection(app.clone()).await;
tracing::info!("Server connection setup");
// we delay spawning this future so that the timeout is created lazily
let cli_health_check = match server_connection {
ServerConnection::CLI {
child,
health_check,
url,
username,
password,
} => {
let app = app.clone();
Some(
async move {
let res = timeout(Duration::from_secs(30), health_check.0).await;
let err = match res {
Ok(Ok(Ok(()))) => None,
Ok(Ok(Err(e))) => Some(e),
Ok(Err(e)) => Some(format!("Health check task failed: {e}")),
Err(_) => Some("Health check timed out".to_string()),
};
if let Some(err) = err {
let _ = child.kill();
return Err(format!(
"Failed to spawn OpenCode Server ({err}). Logs:\n{}",
get_logs()
));
}
tracing::info!("CLI health check OK");
app.state::<ServerState>().set_child(Some(child));
Ok(ServerReadyData {
url,
username,
password,
is_sidecar: true,
})
}
.map(move |res| {
let _ = server_ready_tx.send(res);
}),
)
}
ServerConnection::Existing { url } => {
let _ = server_ready_tx.send(Ok(ServerReadyData {
url: url.to_string(),
username: None,
password: None,
is_sidecar: false,
}));
None
}
};
tracing::info!("server connection started");
if let Some(cli_health_check) = cli_health_check {
if let Some(sqlite_done_rx) = sqlite_done {
let _ = sqlite_done_rx.await;
}
tokio::spawn(cli_health_check);
if let Some(sqlite_done_rx) = sqlite_done {
let _ = sqlite_done_rx.await;
}
let _ = server_ready_rx.await;
// Wait for sidecar to become healthy (for loading window progress)
let res = timeout(Duration::from_secs(30), health_check.0).await;
match res {
Ok(Ok(Ok(()))) => tracing::info!("Sidecar health check OK"),
Ok(Ok(Err(e))) => tracing::error!("Sidecar health check failed: {e}"),
Ok(Err(e)) => tracing::error!("Sidecar health check task failed: {e}"),
Err(_) => tracing::error!("Sidecar health check timed out"),
}
tracing::info!("Loading task finished");
}
@@ -561,7 +500,8 @@ async fn initialize(app: AppHandle) {
.map_err(|_| ())
.shared();
let loading_window = if needs_sqlite_migration
// Show loading window for SQLite migrations if they take >1s
let loading_window = if needs_migration
&& timeout(Duration::from_secs(1), loading_task.clone())
.await
.is_err()
@@ -571,12 +511,12 @@ async fn initialize(app: AppHandle) {
sleep(Duration::from_secs(1)).await;
Some(loading_window)
} else {
tracing::debug!("Showing main window without loading window");
MainWindow::create(&app).expect("Failed to create main window");
None
};
// Create main window immediately - the web app handles its own loading/health gate
MainWindow::create(&app).expect("Failed to create main window");
let _ = loading_task.await;
tracing::info!("Loading done, completing initialisation");
@@ -584,12 +524,9 @@ async fn initialize(app: AppHandle) {
if loading_window.is_some() {
loading_window_complete.await;
tracing::info!("Loading window completed");
}
MainWindow::create(&app).expect("Failed to create main window");
if let Some(loading_window) = loading_window {
let _ = loading_window.close();
}
@@ -610,59 +547,6 @@ fn spawn_cli_sync_task(app: AppHandle) {
});
}
enum ServerConnection {
Existing {
url: String,
},
CLI {
url: String,
username: Option<String>,
password: Option<String>,
child: CommandChild,
health_check: server::HealthCheck,
},
}
async fn setup_server_connection(app: AppHandle) -> ServerConnection {
let custom_url = get_saved_server_url(&app).await;
tracing::info!(?custom_url, "Attempting server connection");
if let Some(url) = &custom_url
&& server::check_health_or_ask_retry(&app, url).await
{
tracing::info!(%url, "Connected to custom server");
// If the default server is already local, no need to also spawn a sidecar
if server::is_localhost_url(url) {
return ServerConnection::Existing { url: url.clone() };
}
// Remote default server: fall through and also spawn a local sidecar
}
let local_port = get_sidecar_port();
let hostname = "127.0.0.1";
let local_url = format!("http://{hostname}:{local_port}");
tracing::debug!(url = %local_url, "Checking health of local server");
if server::check_health(&local_url, None).await {
tracing::info!(url = %local_url, "Health check OK, using existing server");
return ServerConnection::Existing { url: local_url };
}
let password = uuid::Uuid::new_v4().to_string();
tracing::info!("Spawning new local server");
let (child, health_check) =
server::spawn_local_server(app, hostname.to_string(), local_port, password.clone());
ServerConnection::CLI {
url: local_url,
username: Some("opencode".to_string()),
password: Some(password),
child,
health_check,
}
}
fn get_sidecar_port() -> u32 {
option_env!("OPENCODE_PORT")

View File

@@ -1,7 +1,6 @@
use std::time::{Duration, Instant};
use tauri::AppHandle;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_store::StoreExt;
use tokio::task::JoinHandle;
@@ -85,22 +84,6 @@ pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> {
Ok(())
}
pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option<String> {
if let Some(url) = get_default_server_url(app.clone()).ok().flatten() {
tracing::info!(%url, "Using desktop-specific custom URL");
return Some(url);
}
if let Some(cli_config) = cli::get_config(app).await
&& let Some(url) = get_server_url_from_config(&cli_config)
{
tracing::info!(%url, "Using custom server URL from config");
return Some(url);
}
None
}
pub fn spawn_local_server(
app: AppHandle,
hostname: String,
@@ -145,19 +128,27 @@ pub fn spawn_local_server(
pub struct HealthCheck(pub JoinHandle<Result<(), String>>);
pub async fn check_health(url: &str, password: Option<&str>) -> bool {
async fn check_health(url: &str, password: Option<&str>) -> bool {
let Ok(url) = reqwest::Url::parse(url) else {
return false;
};
let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(7));
if url_is_localhost(&url) {
if url
.host_str()
.is_some_and(|host| {
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
})
{
// Some environments set proxy variables (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) without
// excluding loopback. reqwest respects these by default, which can prevent the desktop
// app from reaching its own local sidecar server.
builder = builder.no_proxy();
};
}
let Ok(client) = builder.build() else {
return false;
@@ -177,77 +168,3 @@ pub async fn check_health(url: &str, password: Option<&str>) -> bool {
.map(|r| r.status().is_success())
.unwrap_or(false)
}
pub fn is_localhost_url(url: &str) -> bool {
reqwest::Url::parse(url).is_ok_and(|u| url_is_localhost(&u))
}
fn url_is_localhost(url: &reqwest::Url) -> bool {
url.host_str().is_some_and(|host| {
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
})
}
/// Converts a bind address hostname to a valid URL hostname for connection.
/// - `0.0.0.0` and `::` are wildcard bind addresses, not valid connect targets
/// - IPv6 addresses need brackets in URLs (e.g., `::1` -> `[::1]`)
fn normalize_hostname_for_url(hostname: &str) -> String {
// Wildcard bind addresses -> localhost equivalents
if hostname == "0.0.0.0" {
return "127.0.0.1".to_string();
}
if hostname == "::" {
return "[::1]".to_string();
}
// IPv6 addresses need brackets in URLs
if hostname.contains(':') && !hostname.starts_with('[') {
return format!("[{}]", hostname);
}
hostname.to_string()
}
fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
let server = config.server.as_ref()?;
let port = server.port?;
tracing::debug!(port, "server.port found in OC config");
let hostname = server
.hostname
.as_ref()
.map(|v| normalize_hostname_for_url(v))
.unwrap_or_else(|| "127.0.0.1".to_string());
Some(format!("http://{}:{}", hostname, port))
}
pub async fn check_health_or_ask_retry(app: &AppHandle, url: &str) -> bool {
tracing::debug!(%url, "Checking health");
loop {
if check_health(url, None).await {
return true;
}
const RETRY: &str = "Retry";
let res = app.dialog()
.message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url))
.title("Connection Failed")
.buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string()))
.blocking_show_with_result();
match res {
MessageDialogResult::Custom(name) if name == RETRY => {
continue;
}
_ => {
break;
}
}
}
false
}

View File

@@ -38,7 +38,6 @@ export type ServerReadyData = {
url: string,
username: string | null,
password: string | null,
is_sidecar: boolean,
};
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" };

View File

@@ -9,7 +9,6 @@ import {
ServerConnection,
useCommand,
} from "@opencode-ai/app"
import { Splash } from "@opencode-ai/ui/logo"
import type { AsyncStorage } from "@solid-primitives/storage"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { readImage } from "@tauri-apps/plugin-clipboard-manager"
@@ -22,7 +21,7 @@ import { relaunch } from "@tauri-apps/plugin-process"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { Store } from "@tauri-apps/plugin-store"
import { check, type Update } from "@tauri-apps/plugin-updater"
import { createResource, type JSX, onCleanup, onMount, Show } from "solid-js"
import { createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../package.json"
import { initI18n, t } from "./i18n"
@@ -30,7 +29,7 @@ import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
import { Channel } from "@tauri-apps/api/core"
import { commands, ServerReadyData, type InitStep } from "./bindings"
import { commands, type InitStep } from "./bindings"
import { createMenu } from "./menu"
const root = document.getElementById("root")
@@ -348,12 +347,13 @@ const createPlatform = (): Platform => {
await commands.setWslConfig({ enabled })
},
getDefaultServerUrl: async () => {
const result = await commands.getDefaultServerUrl().catch(() => null)
return result
getDefaultServer: async () => {
const url = await commands.getDefaultServerUrl().catch(() => null)
if (!url) return null
return ServerConnection.Key.make(url)
},
setDefaultServerUrl: async (url: string | null) => {
setDefaultServer: async (url: string | null) => {
await commands.setDefaultServerUrl(url)
},
@@ -412,12 +412,33 @@ void listenForDeepLinks()
render(() => {
const platform = createPlatform()
// Fetch sidecar credentials from Rust (available immediately, before health check)
const [sidecar] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any))
const [defaultServer] = createResource(() =>
platform.getDefaultServerUrl?.().then((url) => {
platform.getDefaultServer?.().then((url) => {
if (url) return ServerConnection.key({ type: "http", http: { url } })
}),
)
// Build the sidecar server connection once credentials arrive
const servers = () => {
const data = sidecar()
if (!data) return []
const http = {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
}
const server: ServerConnection.Sidecar = {
displayName: t("desktop.server.local"),
type: "sidecar",
variant: "base",
http,
}
return [server] as ServerConnection.Any[]
}
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
@@ -426,6 +447,12 @@ render(() => {
}
}
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
return null
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
@@ -436,60 +463,19 @@ render(() => {
return (
<PlatformProvider value={platform}>
<AppBaseProviders>
<ServerGate>
{(data) => {
const http = {
url: data.url,
username: data.username ?? undefined,
password: data.password ?? undefined,
}
const server: ServerConnection.Any = data.is_sidecar
? {
displayName: t("desktop.server.local"),
type: "sidecar",
variant: "base",
http,
}
: { type: "http", http }
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
return null
}
<Show when={!defaultServer.loading && !sidecar.loading}>
{(_) => {
return (
<Show when={!defaultServer.loading}>
<AppInterface defaultServer={defaultServer.latest ?? ServerConnection.key(server)} servers={[server]}>
<Inner />
</AppInterface>
</Show>
<AppInterface
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
servers={servers()}
>
<Inner />
</AppInterface>
)
}}
</ServerGate>
</Show>
</AppBaseProviders>
</PlatformProvider>
)
}, root!)
// Gate component that waits for the server to be ready
function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element }) {
const [serverData] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any))
if (serverData.state === "errored") throw serverData.error
return (
<Show
when={serverData.state !== "pending" && serverData()}
fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
<div data-tauri-decorum-tb class="flex flex-row absolute top-0 right-0 z-10 h-10" />
</div>
}
>
{(data) => props.children(data())}
</Show>
)
}

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