Compare commits

..

3 Commits

Author SHA1 Message Date
Shoubhit Dash 01d80f37dd refactor(app): use button shortcut in shell tray 2026-04-07 19:34:13 +05:30
Shoubhit Dash f714300e9a refactor(ui): add button shortcut component 2026-04-07 19:34:05 +05:30
Shoubhit Dash 30c4ccbfee style(app): update shell mode tray 2026-04-07 18:10:35 +05:30
221 changed files with 1979 additions and 2601 deletions

View File

@ -9,7 +9,6 @@
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"heap-snapshot-toolkit": "1.1.3",
"typescript": "catalog:", "typescript": "catalog:",
}, },
"devDependencies": { "devDependencies": {
@ -27,7 +26,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -81,7 +80,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@ -115,7 +114,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@ -142,7 +141,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@ -166,7 +165,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@ -190,7 +189,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@ -223,7 +222,7 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@ -255,7 +254,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@ -284,7 +283,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@ -300,7 +299,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.4.0", "version": "1.3.17",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@ -436,7 +435,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@ -470,7 +469,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@ -485,7 +484,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@ -520,7 +519,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -533,7 +532,6 @@
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"diff": "catalog:",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"katex": "0.16.27", "katex": "0.16.27",
@ -569,7 +567,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@ -580,7 +578,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.4.0", "version": "1.3.17",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@ -3259,8 +3257,6 @@
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"heap-snapshot-toolkit": ["heap-snapshot-toolkit@1.1.3", "", {}, "sha512-joThu2rEsDu8/l4arupRDI1qP4CZXNG+J6Wr348vnbLGSiBkwRdqZ6aOHl5BzEiC+Dc8OTbMlmWjD0lbXD5K2Q=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],

View File

@ -109,12 +109,6 @@ const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50",
appliesToProducts: [zenLiteProduct.id], appliesToProducts: [zenLiteProduct.id],
duration: "once", duration: "once",
}) })
const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100", {
name: "First month 100% off",
percentOff: 100,
appliesToProducts: [zenLiteProduct.id],
duration: "once",
})
const zenLitePrice = new stripe.Price("ZenLitePrice", { const zenLitePrice = new stripe.Price("ZenLitePrice", {
product: zenLiteProduct.id, product: zenLiteProduct.id,
currency: "usd", currency: "usd",
@ -130,7 +124,6 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
price: zenLitePrice.id, price: zenLitePrice.id,
priceInr: 92900, priceInr: 92900,
firstMonth50Coupon: zenLiteCouponFirstMonth50.id, firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
}, },
}) })
@ -236,7 +229,6 @@ new sst.cloudflare.x.SolidStart("Console", {
SALESFORCE_INSTANCE_URL, SALESFORCE_INSTANCE_URL,
ZEN_BLACK_PRICE, ZEN_BLACK_PRICE,
ZEN_LITE_PRICE, ZEN_LITE_PRICE,
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
new sst.Secret("ZEN_LIMITS"), new sst.Secret("ZEN_LIMITS"),
new sst.Secret("ZEN_SESSION_SECRET"), new sst.Secret("ZEN_SESSION_SECRET"),
...ZEN_MODELS, ...ZEN_MODELS,

View File

@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-85wpU1oCWbthPleNIOj5d5AOuuYZ6rM7gMLZR6YJ2WU=", "x86_64-linux": "sha256-r1+AehuOGIOaaxfXkQGracT/6OdFRn5Ub8s7H+MeKFY=",
"aarch64-linux": "sha256-C3A56SDQGJquCpIRj2JhIzr4A7N4cc9lxtEjl8bXDeM=", "aarch64-linux": "sha256-WkMSRF/ZJLyzxNBjpiMR459C9G0NVOEw31tm8roPneA=",
"aarch64-darwin": "sha256-/Ij3qhGRrcLlMfl9uEacDNnGK5URxhctuQFBW4Njrog=", "aarch64-darwin": "sha256-Z127cxFpTl8Ml7PB3CG9TcCU08oYCPuk0FECK2MQ2CI=",
"x86_64-darwin": "sha256-10sOPuN4eZ75orw4FI8ztCq1+AKS2e8aAfg3Z6Yn56w=" "x86_64-darwin": "sha256-pkRoFtnVjyl+5fm+rrFyRnEwvptxylnFxPAcEv4ZOCg="
} }
} }

View File

@ -91,7 +91,6 @@
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"heap-snapshot-toolkit": "1.1.3",
"typescript": "catalog:" "typescript": "catalog:"
}, },
"repository": { "repository": {

View File

@ -320,7 +320,6 @@ export async function createTestProject(input?: { serverUrl?: string }) {
execSync("git init", { cwd: root, stdio: "ignore" }) execSync("git init", { cwd: root, stdio: "ignore" })
await fs.writeFile(path.join(root, ".git", "opencode"), id) await fs.writeFile(path.join(root, ".git", "opencode"), id)
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" }) execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
execSync("git config commit.gpgsign false", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" }) execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', { execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
cwd: root, cwd: root,

View File

@ -1,6 +1,7 @@
import { seedSessionTask, withSession } from "../actions" import { seedSessionTask, withSession } from "../actions"
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { inputMatch } from "../prompt/mock" import { inputMatch } from "../prompt/mock"
import { promptSelector } from "../selectors"
test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => { test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
@ -29,33 +30,15 @@ test("task tool child-session link does not trigger stale show errors", async ({
await project.gotoSession(session.id) await project.gotoSession(session.id)
const header = page.locator("[data-session-title]") const link = page
await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 }) .locator("a.subagent-link")
const card = page
.locator('[data-component="task-tool-card"]')
.filter({ hasText: /open child session/i }) .filter({ hasText: /open child session/i })
.first() .first()
await expect(card).toBeVisible({ timeout: 30_000 }) await expect(link).toBeVisible({ timeout: 30_000 })
await card.click() await link.click()
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title) await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description)
await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/")
await expect
.poll(
() =>
header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({
left: getComputedStyle(el).paddingLeft,
right: getComputedStyle(el).paddingRight,
})),
{ timeout: 30_000 },
)
.toEqual({ left: "8px", right: "8px" })
await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0)
await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 })
await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 })
await expect.poll(() => errs, { timeout: 5_000 }).toEqual([]) await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
}) })
} finally { } finally {

View File

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

View File

@ -19,6 +19,7 @@ import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { ButtonShortcut } from "@opencode-ai/ui/button-shortcut"
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
@ -1450,42 +1451,41 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show when={store.mode === "normal" || store.mode === "shell"}> <Show when={store.mode === "normal" || store.mode === "shell"}>
<DockTray attach="top"> <DockTray attach="top">
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0"> <div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative"> <div class="flex h-7 items-center gap-1.5 min-w-0 flex-1 relative">
<div <div
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0" class="flex items-center max-w-[160px] min-w-0 absolute inset-y-0 left-0"
style={{ style={{
padding: "0 4px 0 8px", padding: "0 8px",
...shell(), ...shell(),
}} }}
> >
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span> <span class="truncate text-13-regular text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" />
</div> </div>
<div class="flex items-center gap-1.5 min-w-0 flex-1"> <Show when={store.mode !== "shell"}>
<div data-component="prompt-agent-control"> <div class="flex items-center gap-1.5 min-w-0 flex-1">
<TooltipKeybind <div data-component="prompt-agent-control">
placement="top" <TooltipKeybind
gutter={4} placement="top"
title={language.t("command.agent.cycle")} gutter={4}
keybind={command.keybind("agent.cycle")} title={language.t("command.agent.cycle")}
> keybind={command.keybind("agent.cycle")}
<Select >
size="normal" <Select
options={agentNames()} size="normal"
current={local.agent.current()?.name ?? ""} options={agentNames()}
onSelect={(value) => { current={local.agent.current()?.name ?? ""}
local.agent.set(value) onSelect={(value) => {
restoreFocus() local.agent.set(value)
}} restoreFocus()
class="capitalize max-w-[160px] text-text-base" }}
valueClass="truncate text-13-regular text-text-base" class="capitalize max-w-[160px] text-text-base"
triggerStyle={control()} valueClass="truncate text-13-regular text-text-base"
triggerProps={{ "data-action": "prompt-agent" }} triggerStyle={control()}
variant="ghost" triggerProps={{ "data-action": "prompt-agent" }}
/> variant="ghost"
</TooltipKeybind> />
</div> </TooltipKeybind>
<Show when={store.mode !== "shell"}> </div>
<div data-component="prompt-model-control"> <div data-component="prompt-model-control">
<Show <Show
when={providers.paid().length > 0} when={providers.paid().length > 0}
@ -1581,7 +1581,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/> />
</TooltipKeybind> </TooltipKeybind>
</div> </div>
</Show> </div>
</Show>
<div class="absolute inset-y-0 right-0 flex items-center" style={shell()}>
<ButtonShortcut
type="button"
variant="ghost"
size="small"
shortcut="Esc"
shortcutAria="Escape"
class="h-6 gap-2 rounded-[6px] border-none px-0 py-0 pl-3 pr-0.75 text-13-medium text-text-base shadow-none"
tabIndex={store.mode === "shell" ? undefined : -1}
onClick={() => setMode("normal")}
aria-label={language.t("common.cancel")}
>
{language.t("common.cancel")}
</ButtonShortcut>
</div> </div>
</div> </div>
</div> </div>

View File

@ -146,7 +146,7 @@ beforeAll(async () => {
add: (value: { add: (value: {
directory?: string directory?: string
sessionID?: string sessionID?: string
message: { agent: string; model: { providerID: string; modelID: string; variant?: string } } message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
}) => { }) => {
optimistic.push(value) optimistic.push(value)
optimisticSeeded.push( optimisticSeeded.push(
@ -310,7 +310,8 @@ describe("prompt submit worktree selection", () => {
expect(optimistic[0]).toMatchObject({ expect(optimistic[0]).toMatchObject({
message: { message: {
agent: "agent", agent: "agent",
model: { providerID: "provider", modelID: "model", variant: "high" }, model: { providerID: "provider", modelID: "model" },
variant: "high",
}, },
}) })
}) })

View File

@ -121,7 +121,8 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
role: "user", role: "user",
time: { created: Date.now() }, time: { created: Date.now() },
agent: input.draft.agent, agent: input.draft.agent,
model: { ...input.draft.model, variant: input.draft.variant }, model: input.draft.model,
variant: input.draft.variant,
} }
const add = () => const add = () =>

View File

@ -1,6 +1,7 @@
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { import type {
FileDiff,
Message, Message,
Part, Part,
PermissionRequest, PermissionRequest,
@ -8,7 +9,6 @@ import type {
QuestionRequest, QuestionRequest,
Session, Session,
SessionStatus, SessionStatus,
SnapshotFileDiff,
Todo, Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
@ -161,7 +161,7 @@ export function applyDirectoryEvent(input: {
break break
} }
case "session.diff": { case "session.diff": {
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } const props = event.properties as { sessionID: string; diff: FileDiff[] }
input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" })) input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" }))
break break
} }

View File

@ -1,11 +1,11 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { import type {
FileDiff,
Message, Message,
Part, Part,
PermissionRequest, PermissionRequest,
QuestionRequest, QuestionRequest,
SessionStatus, SessionStatus,
SnapshotFileDiff,
Todo, Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@ -33,7 +33,7 @@ describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => { test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: { const store: {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined> session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined> todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>
@ -64,7 +64,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1") const m = msg("msg_1", "ses_1")
const store: { const store: {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined> session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined> todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>

View File

@ -1,10 +1,10 @@
import type { import type {
FileDiff,
Message, Message,
Part, Part,
PermissionRequest, PermissionRequest,
QuestionRequest, QuestionRequest,
SessionStatus, SessionStatus,
SnapshotFileDiff,
Todo, Todo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40
type SessionCache = { type SessionCache = {
session_status: Record<string, SessionStatus | undefined> session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined> session_diff: Record<string, FileDiff[] | undefined>
todo: Record<string, Todo[] | undefined> todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>

View File

@ -2,6 +2,7 @@ import type {
Agent, Agent,
Command, Command,
Config, Config,
FileDiff,
LspStatus, LspStatus,
McpStatus, McpStatus,
Message, Message,
@ -13,7 +14,6 @@ import type {
QuestionRequest, QuestionRequest,
Session, Session,
SessionStatus, SessionStatus,
SnapshotFileDiff,
Todo, Todo,
VcsInfo, VcsInfo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
@ -48,7 +48,7 @@ export type State = {
[sessionID: string]: SessionStatus [sessionID: string]: SessionStatus
} }
session_diff: { session_diff: {
[sessionID: string]: SnapshotFileDiff[] [sessionID: string]: FileDiff[]
} }
todo: { todo: {
[sessionID: string]: Todo[] [sessionID: string]: Todo[]

View File

@ -11,7 +11,7 @@ import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } fro
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import { useSync } from "./sync" import { useSync } from "./sync"
export type ModelKey = { providerID: string; modelID: string; variant?: string } export type ModelKey = { providerID: string; modelID: string }
type State = { type State = {
agent?: string agent?: string
@ -373,7 +373,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
handoff.set(handoffKey(dir, session), next) handoff.set(handoffKey(dir, session), next)
setStore("draft", undefined) setStore("draft", undefined)
}, },
restore(msg: { sessionID: string; agent: string; model: ModelKey }) { restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
const session = id() const session = id()
if (!session) return if (!session) return
if (msg.sessionID !== session) return if (msg.sessionID !== session) return
@ -383,7 +383,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setSaved("session", session, { setSaved("session", session, {
agent: msg.agent, agent: msg.agent,
model: msg.model, model: msg.model,
variant: msg.model.variant ?? null, variant: msg.variant ?? null,
}) })
}, },
}, },

View File

@ -416,7 +416,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
role: "user", role: "user",
time: { created: Date.now() }, time: { created: Date.now() },
agent: input.agent, agent: input.agent,
model: { ...input.model, variant: input.variant }, model: input.model,
variant: input.variant,
} }
const [, setStore] = target() const [, setStore] = target()
setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts }) setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })

View File

@ -238,8 +238,6 @@ export const dict = {
"prompt.mode.shell": "Shell", "prompt.mode.shell": "Shell",
"prompt.mode.normal": "Prompt", "prompt.mode.normal": "Prompt",
"prompt.mode.shell.exit": "esc to exit", "prompt.mode.shell.exit": "esc to exit",
"session.child.promptDisabled": "Subagent sessions cannot be prompted.",
"session.child.backToParent": "Back to main session.",
"prompt.example.1": "Fix a TODO in the codebase", "prompt.example.1": "Fix a TODO in the codebase",
"prompt.example.2": "What is the tech stack of this project?", "prompt.example.2": "What is the tech stack of this project?",

View File

@ -1,46 +1,6 @@
@import "@opencode-ai/ui/styles/tailwind"; @import "@opencode-ai/ui/styles/tailwind";
@layer components { @layer components {
@keyframes session-progress-whip {
0% {
clip-path: inset(0 100% 0 0 round 999px);
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}
48% {
clip-path: inset(0 0 0 0 round 999px);
animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
}
100% {
clip-path: inset(0 0 0 100% round 999px);
}
}
[data-component="session-progress"] {
position: absolute;
inset: 0 0 auto;
height: 2px;
overflow: hidden;
pointer-events: none;
opacity: 1;
transition: opacity 220ms ease-out;
}
[data-component="session-progress"][data-state="hiding"] {
opacity: 0;
}
[data-component="session-progress-bar"] {
width: 100%;
height: 100%;
border-radius: 999px;
background: var(--session-progress-color);
clip-path: inset(0 100% 0 0 round 999px);
animation: session-progress-whip var(--session-progress-ms, 1800ms) infinite;
will-change: clip-path;
}
[data-component="getting-started"] { [data-component="getting-started"] {
container-type: inline-size; container-type: inline-size;
container-name: getting-started; container-name: getting-started;

View File

@ -150,6 +150,7 @@ export default function Layout(props: ParentProps) {
const [state, setState] = createStore({ const [state, setState] = createStore({
autoselect: !initialDirectory, autoselect: !initialDirectory,
busyWorkspaces: {} as Record<string, boolean>, busyWorkspaces: {} as Record<string, boolean>,
hoverSession: undefined as string | undefined,
hoverProject: undefined as string | undefined, hoverProject: undefined as string | undefined,
scrollSessionKey: undefined as string | undefined, scrollSessionKey: undefined as string | undefined,
nav: undefined as HTMLElement | undefined, nav: undefined as HTMLElement | undefined,
@ -193,6 +194,7 @@ export default function Layout(props: ParentProps) {
onActivate: (directory) => { onActivate: (directory) => {
globalSync.child(directory) globalSync.child(directory)
setState("hoverProject", directory) setState("hoverProject", directory)
setState("hoverSession", undefined)
}, },
}) })
@ -229,6 +231,7 @@ export default function Layout(props: ParentProps) {
aim.reset() aim.reset()
} }
const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined)) const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
const disarm = () => { const disarm = () => {
if (navLeave.current === undefined) return if (navLeave.current === undefined) return
@ -238,6 +241,7 @@ export default function Layout(props: ParentProps) {
const reset = () => { const reset = () => {
disarm() disarm()
setState("hoverSession", undefined)
setHoverProject(undefined) setHoverProject(undefined)
} }
@ -248,6 +252,7 @@ export default function Layout(props: ParentProps) {
navLeave.current = window.setTimeout(() => { navLeave.current = window.setTimeout(() => {
navLeave.current = undefined navLeave.current = undefined
setHoverProject(undefined) setHoverProject(undefined)
setState("hoverSession", undefined)
}, 300) }, 300)
} }
@ -1967,6 +1972,9 @@ export default function Layout(props: ParentProps) {
navList: currentSessions, navList: currentSessions,
sidebarExpanded, sidebarExpanded,
sidebarHovering, sidebarHovering,
nav: () => state.nav,
hoverSession: () => state.hoverSession,
setHoverSession,
clearHoverProjectSoon, clearHoverProjectSoon,
prefetchSession, prefetchSession,
archiveSession, archiveSession,
@ -1995,6 +2003,7 @@ export default function Layout(props: ParentProps) {
sidebarOpened: () => layout.sidebar.opened(), sidebarOpened: () => layout.sidebar.opened(),
sidebarHovering, sidebarHovering,
hoverProject: () => state.hoverProject, hoverProject: () => state.hoverProject,
nav: () => state.nav,
onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
onProjectMouseLeave: (worktree) => aim.leave(worktree), onProjectMouseLeave: (worktree) => aim.leave(worktree),
onProjectFocus: (worktree) => aim.activate(worktree), onProjectFocus: (worktree) => aim.activate(worktree),
@ -2013,10 +2022,15 @@ export default function Layout(props: ParentProps) {
sessionProps: { sessionProps: {
navList: currentSessions, navList: currentSessions,
sidebarExpanded, sidebarExpanded,
sidebarHovering,
nav: () => state.nav,
hoverSession: () => state.hoverSession,
setHoverSession,
clearHoverProjectSoon, clearHoverProjectSoon,
prefetchSession, prefetchSession,
archiveSession, archiveSession,
}, },
setHoverSession,
} }
const SidebarPanel = (panelProps: { const SidebarPanel = (panelProps: {
@ -2027,6 +2041,7 @@ export default function Layout(props: ParentProps) {
const project = panelProps.project const project = panelProps.project
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened())
const empty = createMemo(() => !params.dir && layout.projects.list().length === 0) const empty = createMemo(() => !params.dir && layout.projects.list().length === 0)
const projectName = createMemo(() => { const projectName = createMemo(() => {
const item = project() const item = project()
@ -2228,6 +2243,7 @@ export default function Layout(props: ParentProps) {
project={project()!} project={project()!}
sortNow={sortNow} sortNow={sortNow}
mobile={panelProps.mobile} mobile={panelProps.mobile}
popover={popover()}
/> />
</div> </div>
</> </>
@ -2272,6 +2288,7 @@ export default function Layout(props: ParentProps) {
project={project()!} project={project()!}
sortNow={sortNow} sortNow={sortNow}
mobile={panelProps.mobile} mobile={panelProps.mobile}
popover={popover()}
/> />
)} )}
</For> </For>

View File

@ -8,7 +8,6 @@ import {
} from "./deep-links" } from "./deep-links"
import { type Session } from "@opencode-ai/sdk/v2/client" import { type Session } from "@opencode-ai/sdk/v2/client"
import { import {
childSessionOnPath,
displayName, displayName,
effectiveWorkspaceOrder, effectiveWorkspaceOrder,
errorMessage, errorMessage,
@ -199,19 +198,6 @@ describe("layout workspace helpers", () => {
expect(result?.id).toBe("root") expect(result?.id).toBe("root")
}) })
test("finds the direct child on the active session path", () => {
const list = [
session({ id: "root", directory: "/workspace" }),
session({ id: "child", directory: "/workspace", parentID: "root" }),
session({ id: "leaf", directory: "/workspace", parentID: "child" }),
]
expect(childSessionOnPath(list, "root", "leaf")?.id).toBe("child")
expect(childSessionOnPath(list, "child", "leaf")?.id).toBe("leaf")
expect(childSessionOnPath(list, "root", "root")).toBeUndefined()
expect(childSessionOnPath(list, "root", "other")).toBeUndefined()
})
test("formats fallback project display name", () => { test("formats fallback project display name", () => {
expect(displayName({ worktree: "/tmp/app" })).toBe("app") expect(displayName({ worktree: "/tmp/app" })).toBe("app")
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App") expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")

View File

@ -46,17 +46,18 @@ export function hasProjectPermissions<T>(
return Object.values(request ?? {}).some((list) => list?.some(include)) return Object.values(request ?? {}).some((list) => list?.some(include))
} }
export const childSessionOnPath = (sessions: Session[] | undefined, rootID: string, activeID?: string) => { export const childMapByParent = (sessions: Session[] | undefined) => {
if (!activeID || activeID === rootID) return const map = new Map<string, string[]>()
const map = new Map((sessions ?? []).map((session) => [session.id, session])) for (const session of sessions ?? []) {
let id = activeID if (!session.parentID) continue
const existing = map.get(session.parentID)
while (id) { if (existing) {
const session = map.get(id) existing.push(session.id)
if (!session?.parentID) return continue
if (session.parentID === rootID) return session }
id = session.parentID map.set(session.parentID, [session.id])
} }
return map
} }
export const displayName = (project: { name?: string; worktree: string }) => export const displayName = (project: { name?: string; worktree: string }) =>

View File

@ -1,12 +1,15 @@
import type { Session } from "@opencode-ai/sdk/v2/client" import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
import { Avatar } from "@opencode-ai/ui/avatar" import { Avatar } from "@opencode-ai/ui/avatar"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { MessageNav } from "@opencode-ai/ui/message-nav"
import { Spinner } from "@opencode-ai/ui/spinner" import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router" import { A, useNavigate, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
@ -15,7 +18,7 @@ import { usePermission } from "@/context/permission"
import { messageAgentColor } from "@/utils/agent" import { messageAgentColor } from "@/utils/agent"
import { sessionTitle } from "@/utils/session-title" import { sessionTitle } from "@/utils/session-title"
import { sessionPermissionRequest } from "../session/composer/session-request-tree" import { sessionPermissionRequest } from "../session/composer/session-request-tree"
import { childSessionOnPath, hasProjectPermissions } from "./helpers" import { hasProjectPermissions } from "./helpers"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@ -36,7 +39,6 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
) )
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0)) const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
return ( return (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}> <div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
<div class="size-full rounded overflow-clip"> <div class="size-full rounded overflow-clip">
@ -71,10 +73,13 @@ export type SessionItemProps = {
slug: string slug: string
mobile?: boolean mobile?: boolean
dense?: boolean dense?: boolean
showTooltip?: boolean popover?: boolean
showChild?: boolean children: Map<string, string[]>
level?: number
sidebarExpanded: Accessor<boolean> sidebarExpanded: Accessor<boolean>
sidebarHovering: Accessor<boolean>
nav: Accessor<HTMLElement | undefined>
hoverSession: Accessor<string | undefined>
setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void clearHoverProjectSoon: () => void
prefetchSession: (session: Session, priority?: "high" | "low") => void prefetchSession: (session: Session, priority?: "high" | "low") => void
archiveSession: (session: Session) => Promise<void> archiveSession: (session: Session) => Promise<void>
@ -90,52 +95,116 @@ const SessionRow = (props: {
hasPermissions: Accessor<boolean> hasPermissions: Accessor<boolean>
hasError: Accessor<boolean> hasError: Accessor<boolean>
unseenCount: Accessor<number> unseenCount: Accessor<number>
setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void clearHoverProjectSoon: () => void
sidebarOpened: Accessor<boolean> sidebarOpened: Accessor<boolean>
warmHover: () => void
warmPress: () => void warmPress: () => void
warmFocus: () => void warmFocus: () => void
}): JSX.Element => { cancelHoverPrefetch: () => void
}) => {
const title = () => sessionTitle(props.session.title) const title = () => sessionTitle(props.session.title)
return ( return (
<A <A
href={`/${props.slug}/session/${props.session.id}`} href={`/${props.slug}/session/${props.session.id}`}
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`} class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onPointerDown={props.warmPress} onPointerDown={props.warmPress}
onPointerEnter={props.warmHover}
onPointerLeave={props.cancelHoverPrefetch}
onFocus={props.warmFocus} onFocus={props.warmFocus}
onClick={() => { onClick={() => {
props.setHoverSession(undefined)
if (props.sidebarOpened()) return if (props.sidebarOpened()) return
props.clearHoverProjectSoon() props.clearHoverProjectSoon()
}} }}
> >
<Show when={props.isWorking() || props.hasPermissions() || props.hasError() || props.unseenCount() > 0}> <div
<div class="shrink-0 size-6 flex items-center justify-center"
class="shrink-0 size-6 flex items-center justify-center" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} >
> <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
<Switch> <Match when={props.isWorking()}>
<Match when={props.isWorking()}> <Spinner class="size-[15px]" />
<Spinner class="size-[15px]" /> </Match>
</Match> <Match when={props.hasPermissions()}>
<Match when={props.hasPermissions()}> <div class="size-1.5 rounded-full bg-surface-warning-strong" />
<div class="size-1.5 rounded-full bg-surface-warning-strong" /> </Match>
</Match> <Match when={props.hasError()}>
<Match when={props.hasError()}> <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
<div class="size-1.5 rounded-full bg-text-diff-delete-base" /> </Match>
</Match> <Match when={props.unseenCount() > 0}>
<Match when={props.unseenCount() > 0}> <div class="size-1.5 rounded-full bg-text-interactive-base" />
<div class="size-1.5 rounded-full bg-text-interactive-base" /> </Match>
</Match> </Switch>
</Switch> </div>
</div>
</Show>
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span> <span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span>
</A> </A>
) )
} }
const SessionHoverPreview = (props: {
mobile?: boolean
nav: Accessor<HTMLElement | undefined>
hoverSession: Accessor<string | undefined>
session: Session
sidebarHovering: Accessor<boolean>
hoverReady: Accessor<boolean>
hoverMessages: Accessor<UserMessage[] | undefined>
language: ReturnType<typeof useLanguage>
isActive: Accessor<boolean>
slug: string
setHoverSession: (id: string | undefined) => void
messageLabel: (message: Message) => string | undefined
onMessageSelect: (message: Message) => void
trigger: JSX.Element
}): JSX.Element => {
let ref: HTMLDivElement | undefined
return (
<HoverCard
openDelay={1000}
closeDelay={props.sidebarHovering() ? 600 : 0}
placement="right-start"
gutter={16}
shift={-2}
trigger={
<div ref={ref} class="min-w-0 w-full">
{props.trigger}
</div>
}
open={props.hoverSession() === props.session.id}
onOpenChange={(open) => {
if (!open) {
props.setHoverSession(undefined)
return
}
if (!ref?.matches(":hover")) return
props.setHoverSession(props.session.id)
}}
>
<Show
when={props.hoverReady()}
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
>
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
<MessageNav
messages={props.hoverMessages() ?? []}
current={undefined}
getLabel={props.messageLabel}
onMessageSelect={props.onMessageSelect}
size="normal"
class="w-60"
/>
</div>
</Show>
</HoverCard>
)
}
export const SessionItem = (props: SessionItemProps): JSX.Element => { export const SessionItem = (props: SessionItemProps): JSX.Element => {
const params = useParams() const params = useParams()
const navigate = useNavigate()
const layout = useLayout() const layout = useLayout()
const language = useLanguage() const language = useLanguage()
const notification = useNotification() const notification = useNotification()
@ -165,13 +234,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
) )
}) })
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)) const tint = createMemo(() => {
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded())) return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
const currentChild = createMemo(() => {
if (!props.showChild) return
return childSessionOnPath(sessionStore.session, props.session.id, params.id)
}) })
const hoverMessages = createMemo(() =>
sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
)
const hoverReady = createMemo(() => hoverMessages() !== undefined)
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => props.session.id === params.id)
const warm = (span: number, priority: "high" | "low") => { const warm = (span: number, priority: "high" | "low") => {
const nav = props.navList?.() const nav = props.navList?.()
const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory) const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory)
@ -192,6 +266,30 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
} }
} }
const hoverPrefetch = {
current: undefined as ReturnType<typeof setTimeout> | undefined,
}
const cancelHoverPrefetch = () => {
if (hoverPrefetch.current === undefined) return
clearTimeout(hoverPrefetch.current)
hoverPrefetch.current = undefined
}
const scheduleHoverPrefetch = () => {
warm(1, "high")
if (hoverPrefetch.current !== undefined) return
hoverPrefetch.current = setTimeout(() => {
hoverPrefetch.current = undefined
warm(2, "low")
}, 80)
}
onCleanup(cancelHoverPrefetch)
const messageLabel = (message: Message) => {
const parts = sessionStore.part[message.id] ?? []
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
return text?.text
}
const item = ( const item = (
<SessionRow <SessionRow
session={props.session} session={props.session}
@ -203,74 +301,86 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
hasPermissions={hasPermissions} hasPermissions={hasPermissions}
hasError={hasError} hasError={hasError}
unseenCount={unseenCount} unseenCount={unseenCount}
setHoverSession={props.setHoverSession}
clearHoverProjectSoon={props.clearHoverProjectSoon} clearHoverProjectSoon={props.clearHoverProjectSoon}
sidebarOpened={layout.sidebar.opened} sidebarOpened={layout.sidebar.opened}
warmHover={scheduleHoverPrefetch}
warmPress={() => warm(2, "high")} warmPress={() => warm(2, "high")}
warmFocus={() => warm(2, "high")} warmFocus={() => warm(2, "high")}
cancelHoverPrefetch={cancelHoverPrefetch}
/> />
) )
return ( return (
<> <div
<div data-session-id={props.session.id}
data-session-id={props.session.id} class="group/session relative w-full min-w-0 rounded-md cursor-default pl-2 pr-3 transition-colors
class="group/session relative w-full min-w-0 rounded-md cursor-default pr-3 transition-colors hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active" hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
style={{ "padding-left": `${8 + (props.level ?? 0) * 16}px` }} >
> <div class="flex min-w-0 items-center gap-1">
<div class="flex min-w-0 items-center gap-1"> <div class="min-w-0 flex-1">
<div class="min-w-0 flex-1"> <Show
<Show when={hoverEnabled()}
when={!tooltip()} fallback={
fallback={ <Tooltip
<Tooltip placement={props.mobile ? "bottom" : "right"}
placement={props.mobile ? "bottom" : "right"} value={sessionTitle(props.session.title)}
value={sessionTitle(props.session.title)} gutter={10}
gutter={10} class="min-w-0 w-full"
class="min-w-0 w-full" >
> {item}
{item}
</Tooltip>
}
>
{item}
</Show>
</div>
<Show when={!props.level}>
<div
class="shrink-0 overflow-hidden transition-[width,opacity]"
classList={{
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
"w-0 opacity-0 pointer-events-none": !props.mobile,
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
}}
>
<Tooltip value={language.t("common.archive")} placement="top">
<IconButton
icon="archive"
variant="ghost"
class="size-6 rounded-md"
aria-label={language.t("common.archive")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
void props.archiveSession(props.session)
}}
/>
</Tooltip> </Tooltip>
</div> }
>
<SessionHoverPreview
mobile={props.mobile}
nav={props.nav}
hoverSession={props.hoverSession}
session={props.session}
sidebarHovering={props.sidebarHovering}
hoverReady={hoverReady}
hoverMessages={hoverMessages}
language={language}
isActive={isActive}
slug={props.slug}
setHoverSession={props.setHoverSession}
messageLabel={messageLabel}
onMessageSelect={(message) => {
if (!isActive())
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
}}
trigger={item}
/>
</Show> </Show>
</div> </div>
<div
class="shrink-0 overflow-hidden transition-[width,opacity]"
classList={{
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
"w-0 opacity-0 pointer-events-none": !props.mobile,
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
}}
>
<Tooltip value={language.t("common.archive")} placement="top">
<IconButton
icon="archive"
variant="ghost"
class="size-6 rounded-md"
aria-label={language.t("common.archive")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
void props.archiveSession(props.session)
}}
/>
</Tooltip>
</div>
</div> </div>
<Show when={currentChild()}> </div>
{(child) => (
<div class="w-full">
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
</div>
)}
</Show>
</>
) )
} }
@ -280,6 +390,7 @@ export const NewSessionItem = (props: {
dense?: boolean dense?: boolean
sidebarExpanded: Accessor<boolean> sidebarExpanded: Accessor<boolean>
clearHoverProjectSoon: () => void clearHoverProjectSoon: () => void
setHoverSession: (id: string | undefined) => void
}): JSX.Element => { }): JSX.Element => {
const layout = useLayout() const layout = useLayout()
const language = useLanguage() const language = useLanguage()
@ -289,8 +400,9 @@ export const NewSessionItem = (props: {
<A <A
href={`/${props.slug}/session`} href={`/${props.slug}/session`}
end end
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`} class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onClick={() => { onClick={() => {
props.setHoverSession(undefined)
if (layout.sidebar.opened()) return if (layout.sidebar.opened()) return
props.clearHoverProjectSoon() props.clearHoverProjectSoon()
}} }}

View File

@ -1,4 +1,4 @@
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/util/encode" import { base64Encode } from "@opencode-ai/util/encode"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
@ -11,7 +11,7 @@ import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
import { displayName, sortedRootSessions } from "./helpers" import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
export type ProjectSidebarContext = { export type ProjectSidebarContext = {
currentDir: Accessor<string> currentDir: Accessor<string>
@ -19,6 +19,7 @@ export type ProjectSidebarContext = {
sidebarOpened: Accessor<boolean> sidebarOpened: Accessor<boolean>
sidebarHovering: Accessor<boolean> sidebarHovering: Accessor<boolean>
hoverProject: Accessor<string | undefined> hoverProject: Accessor<string | undefined>
nav: Accessor<HTMLElement | undefined>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void onProjectMouseLeave: (worktree: string) => void
onProjectFocus: (worktree: string) => void onProjectFocus: (worktree: string) => void
@ -31,7 +32,8 @@ export type ProjectSidebarContext = {
workspacesEnabled: (project: LocalProject) => boolean workspacesEnabled: (project: LocalProject) => boolean
workspaceIds: (project: LocalProject) => string[] workspaceIds: (project: LocalProject) => string[]
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "mobile" | "dense"> sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
setHoverSession: (id: string | undefined) => void
} }
export const ProjectDragOverlay = (props: { export const ProjectDragOverlay = (props: {
@ -53,6 +55,7 @@ export const ProjectDragOverlay = (props: {
const ProjectTile = (props: { const ProjectTile = (props: {
project: LocalProject project: LocalProject
mobile?: boolean mobile?: boolean
nav: Accessor<HTMLElement | undefined>
sidebarHovering: Accessor<boolean> sidebarHovering: Accessor<boolean>
selected: Accessor<boolean> selected: Accessor<boolean>
active: Accessor<boolean> active: Accessor<boolean>
@ -192,7 +195,9 @@ const ProjectPreviewPanel = (props: {
workspaces: Accessor<string[]> workspaces: Accessor<string[]>
label: (directory: string) => string label: (directory: string) => string
projectSessions: Accessor<ReturnType<typeof sortedRootSessions>> projectSessions: Accessor<ReturnType<typeof sortedRootSessions>>
projectChildren: Accessor<Map<string, string[]>>
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions> workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
workspaceChildren: (directory: string) => Map<string, string[]>
ctx: ProjectSidebarContext ctx: ProjectSidebarContext
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}): JSX.Element => ( }): JSX.Element => (
@ -213,8 +218,9 @@ const ProjectPreviewPanel = (props: {
list={props.projectSessions()} list={props.projectSessions()}
slug={base64Encode(props.project.worktree)} slug={base64Encode(props.project.worktree)}
dense dense
showTooltip
mobile={props.mobile} mobile={props.mobile}
popover={false}
children={props.projectChildren()}
/> />
)} )}
</For> </For>
@ -223,6 +229,7 @@ const ProjectPreviewPanel = (props: {
<For each={props.workspaces()}> <For each={props.workspaces()}>
{(directory) => { {(directory) => {
const sessions = createMemo(() => props.workspaceSessions(directory)) const sessions = createMemo(() => props.workspaceSessions(directory))
const children = createMemo(() => props.workspaceChildren(directory))
return ( return (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="px-2 py-0.5 flex items-center gap-1 min-w-0"> <div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
@ -239,8 +246,9 @@ const ProjectPreviewPanel = (props: {
list={sessions()} list={sessions()}
slug={base64Encode(directory)} slug={base64Encode(directory)}
dense dense
showTooltip
mobile={props.mobile} mobile={props.mobile}
popover={false}
children={children()}
/> />
)} )}
</For> </For>
@ -302,14 +310,20 @@ export const SortableProject = (props: {
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
const projectChildren = createMemo(() => childMapByParent(projectStore().session))
const workspaceSessions = (directory: string) => { const workspaceSessions = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false }) const [data] = globalSync.child(directory, { bootstrap: false })
return sortedRootSessions(data, props.sortNow()) return sortedRootSessions(data, props.sortNow())
} }
const workspaceChildren = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false })
return childMapByParent(data.session)
}
const tile = () => ( const tile = () => (
<ProjectTile <ProjectTile
project={props.project} project={props.project}
mobile={props.mobile} mobile={props.mobile}
nav={props.ctx.nav}
sidebarHovering={props.ctx.sidebarHovering} sidebarHovering={props.ctx.sidebarHovering}
selected={selected} selected={selected}
active={active} active={active}
@ -346,6 +360,7 @@ export const SortableProject = (props: {
if (state.menu) return if (state.menu) return
if (value && state.suppressHover) return if (value && state.suppressHover) return
props.ctx.onHoverOpenChanged(props.project.worktree, value) props.ctx.onHoverOpenChanged(props.project.worktree, value)
if (value) props.ctx.setHoverSession(undefined)
}} }}
> >
<ProjectPreviewPanel <ProjectPreviewPanel
@ -356,7 +371,9 @@ export const SortableProject = (props: {
workspaces={workspaces} workspaces={workspaces}
label={label} label={label}
projectSessions={projectSessions} projectSessions={projectSessions}
projectChildren={projectChildren}
workspaceSessions={workspaceSessions} workspaceSessions={workspaceSessions}
workspaceChildren={workspaceChildren}
ctx={props.ctx} ctx={props.ctx}
language={language} language={language}
/> />

View File

@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
import { sortedRootSessions, workspaceKey } from "./helpers" import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
type InlineEditorComponent = (props: { type InlineEditorComponent = (props: {
id: string id: string
@ -35,6 +35,9 @@ export type WorkspaceSidebarContext = {
navList: Accessor<Session[]> navList: Accessor<Session[]>
sidebarExpanded: Accessor<boolean> sidebarExpanded: Accessor<boolean>
sidebarHovering: Accessor<boolean> sidebarHovering: Accessor<boolean>
nav: Accessor<HTMLElement | undefined>
hoverSession: Accessor<string | undefined>
setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void clearHoverProjectSoon: () => void
prefetchSession: (session: Session, priority?: "high" | "low") => void prefetchSession: (session: Session, priority?: "high" | "low") => void
archiveSession: (session: Session) => Promise<void> archiveSession: (session: Session) => Promise<void>
@ -149,6 +152,7 @@ const WorkspaceActions = (props: {
showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
root: string root: string
setHoverSession: WorkspaceSidebarContext["setHoverSession"]
clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
navigateToNewSession: () => void navigateToNewSession: () => void
}): JSX.Element => ( }): JSX.Element => (
@ -222,6 +226,7 @@ const WorkspaceActions = (props: {
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
props.setHoverSession(undefined)
props.clearHoverProjectSoon() props.clearHoverProjectSoon()
props.navigateToNewSession() props.navigateToNewSession()
}} }}
@ -234,10 +239,12 @@ const WorkspaceActions = (props: {
const WorkspaceSessionList = (props: { const WorkspaceSessionList = (props: {
slug: Accessor<string> slug: Accessor<string>
mobile?: boolean mobile?: boolean
popover?: boolean
ctx: WorkspaceSidebarContext ctx: WorkspaceSidebarContext
showNew: Accessor<boolean> showNew: Accessor<boolean>
loading: Accessor<boolean> loading: Accessor<boolean>
sessions: Accessor<Session[]> sessions: Accessor<Session[]>
children: Accessor<Map<string, string[]>>
hasMore: Accessor<boolean> hasMore: Accessor<boolean>
loadMore: () => Promise<void> loadMore: () => Promise<void>
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
@ -249,6 +256,7 @@ const WorkspaceSessionList = (props: {
mobile={props.mobile} mobile={props.mobile}
sidebarExpanded={props.ctx.sidebarExpanded} sidebarExpanded={props.ctx.sidebarExpanded}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
setHoverSession={props.ctx.setHoverSession}
/> />
</Show> </Show>
<Show when={props.loading()}> <Show when={props.loading()}>
@ -262,8 +270,13 @@ const WorkspaceSessionList = (props: {
navList={props.ctx.navList} navList={props.ctx.navList}
slug={props.slug()} slug={props.slug()}
mobile={props.mobile} mobile={props.mobile}
showChild popover={props.popover}
children={props.children()}
sidebarExpanded={props.ctx.sidebarExpanded} sidebarExpanded={props.ctx.sidebarExpanded}
sidebarHovering={props.ctx.sidebarHovering}
nav={props.ctx.nav}
hoverSession={props.ctx.hoverSession}
setHoverSession={props.ctx.setHoverSession}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
prefetchSession={props.ctx.prefetchSession} prefetchSession={props.ctx.prefetchSession}
archiveSession={props.ctx.archiveSession} archiveSession={props.ctx.archiveSession}
@ -294,6 +307,7 @@ export const SortableWorkspace = (props: {
project: LocalProject project: LocalProject
sortNow: Accessor<number> sortNow: Accessor<number>
mobile?: boolean mobile?: boolean
popover?: boolean
}): JSX.Element => { }): JSX.Element => {
const navigate = useNavigate() const navigate = useNavigate()
const params = useParams() const params = useParams()
@ -307,6 +321,7 @@ export const SortableWorkspace = (props: {
}) })
const slug = createMemo(() => base64Encode(props.directory)) const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
const children = createMemo(() => childMapByParent(workspaceStore.session))
const local = createMemo(() => props.directory === props.project.worktree) const local = createMemo(() => props.directory === props.project.worktree)
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory)) const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
const workspaceValue = createMemo(() => { const workspaceValue = createMemo(() => {
@ -413,6 +428,7 @@ export const SortableWorkspace = (props: {
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
root={props.project.worktree} root={props.project.worktree}
setHoverSession={props.ctx.setHoverSession}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
navigateToNewSession={() => navigate(`/${slug()}/session`)} navigateToNewSession={() => navigate(`/${slug()}/session`)}
/> />
@ -424,10 +440,12 @@ export const SortableWorkspace = (props: {
<WorkspaceSessionList <WorkspaceSessionList
slug={slug} slug={slug}
mobile={props.mobile} mobile={props.mobile}
popover={props.popover}
ctx={props.ctx} ctx={props.ctx}
showNew={showNew} showNew={showNew}
loading={loading} loading={loading}
sessions={sessions} sessions={sessions}
children={children}
hasMore={hasMore} hasMore={hasMore}
loadMore={loadMore} loadMore={loadMore}
language={language} language={language}
@ -443,6 +461,7 @@ export const LocalWorkspace = (props: {
project: LocalProject project: LocalProject
sortNow: Accessor<number> sortNow: Accessor<number>
mobile?: boolean mobile?: boolean
popover?: boolean
}): JSX.Element => { }): JSX.Element => {
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const language = useLanguage() const language = useLanguage()
@ -452,6 +471,7 @@ export const LocalWorkspace = (props: {
}) })
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const children = createMemo(() => childMapByParent(workspace().store.session))
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const loading = createMemo(() => !booted() && count() === 0) const loading = createMemo(() => !booted() && count() === 0)
@ -469,10 +489,12 @@ export const LocalWorkspace = (props: {
<WorkspaceSessionList <WorkspaceSessionList
slug={slug} slug={slug}
mobile={props.mobile} mobile={props.mobile}
popover={props.popover}
ctx={props.ctx} ctx={props.ctx}
showNew={() => false} showNew={() => false}
loading={loading} loading={loading}
sessions={sessions} sessions={sessions}
children={children}
hasMore={hasMore} hasMore={hasMore}
loadMore={loadMore} loadMore={loadMore}
language={language} language={language}

View File

@ -1,4 +1,4 @@
import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { FileDiff, Project, UserMessage } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMutation } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import { import {
@ -68,7 +68,7 @@ type FollowupItem = FollowupDraft & { id: string }
type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context"> type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context">
const emptyFollowups: FollowupItem[] = [] const emptyFollowups: FollowupItem[] = []
type ChangeMode = "git" | "branch" | "turn" type ChangeMode = "git" | "branch" | "session" | "turn"
type VcsMode = "git" | "branch" type VcsMode = "git" | "branch"
type SessionHistoryWindowInput = { type SessionHistoryWindowInput = {
@ -429,7 +429,6 @@ export default function Page() {
} }
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const isChildSession = createMemo(() => !!info()?.parentID)
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
const hasSessionReview = createMemo(() => sessionCount() > 0) const hasSessionReview = createMemo(() => sessionCount() > 0)
@ -463,6 +462,13 @@ export default function Page() {
if (!id) return false if (!id) return false
return sync.session.history.loading(id) return sync.session.history.loading(id)
}) })
const diffsReady = createMemo(() => {
const id = params.id
if (!id) return true
if (!hasSessionReview()) return true
return sync.data.session_diff[id] !== undefined
})
const userMessages = createMemo( const userMessages = createMemo(
() => messages().filter((m) => m.role === "user") as UserMessage[], () => messages().filter((m) => m.role === "user") as UserMessage[],
emptyUserMessages, emptyUserMessages,
@ -520,19 +526,10 @@ export default function Page() {
deferRender: false, deferRender: false,
}) })
const [vcs, setVcs] = createStore<{ const [vcs, setVcs] = createStore({
diff: { diff: {
git: VcsFileDiff[] git: [] as FileDiff[],
branch: VcsFileDiff[] branch: [] as FileDiff[],
}
ready: {
git: boolean
branch: boolean
}
}>({
diff: {
git: [] as VcsFileDiff[],
branch: [] as VcsFileDiff[],
}, },
ready: { ready: {
git: false, git: false,
@ -650,7 +647,6 @@ export default function Page() {
}, desktopReviewOpen()) }, desktopReviewOpen())
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? []) const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git")
const changesOptions = createMemo<ChangeMode[]>(() => { const changesOptions = createMemo<ChangeMode[]>(() => {
const list: ChangeMode[] = [] const list: ChangeMode[] = []
if (sync.project?.vcs === "git") list.push("git") if (sync.project?.vcs === "git") list.push("git")
@ -662,7 +658,7 @@ export default function Page() {
) { ) {
list.push("branch") list.push("branch")
} }
list.push("turn") list.push("session", "turn")
return list return list
}) })
const vcsMode = createMemo<VcsMode | undefined>(() => { const vcsMode = createMemo<VcsMode | undefined>(() => {
@ -671,17 +667,20 @@ export default function Page() {
const reviewDiffs = createMemo(() => { const reviewDiffs = createMemo(() => {
if (store.changes === "git") return vcs.diff.git if (store.changes === "git") return vcs.diff.git
if (store.changes === "branch") return vcs.diff.branch if (store.changes === "branch") return vcs.diff.branch
if (store.changes === "session") return diffs()
return turnDiffs() return turnDiffs()
}) })
const reviewCount = createMemo(() => { const reviewCount = createMemo(() => {
if (store.changes === "git") return vcs.diff.git.length if (store.changes === "git") return vcs.diff.git.length
if (store.changes === "branch") return vcs.diff.branch.length if (store.changes === "branch") return vcs.diff.branch.length
if (store.changes === "session") return sessionCount()
return turnDiffs().length return turnDiffs().length
}) })
const hasReview = createMemo(() => reviewCount() > 0) const hasReview = createMemo(() => reviewCount() > 0)
const reviewReady = createMemo(() => { const reviewReady = createMemo(() => {
if (store.changes === "git") return vcs.ready.git if (store.changes === "git") return vcs.ready.git
if (store.changes === "branch") return vcs.ready.branch if (store.changes === "branch") return vcs.ready.branch
if (store.changes === "session") return !hasSessionReview() || diffsReady()
return true return true
}) })
@ -749,6 +748,13 @@ export default function Page() {
scrollToMessage(msgs[targetIndex], "auto") scrollToMessage(msgs[targetIndex], "auto")
} }
const sessionEmptyKey = createMemo(() => {
const project = sync.project
if (project && !project.vcs) return "session.review.noVcs"
if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
return "session.review.empty"
})
function upsert(next: Project) { function upsert(next: Project) {
const list = globalSync.data.project const list = globalSync.data.project
sync.set("project", next.id) sync.set("project", next.id)
@ -1052,7 +1058,7 @@ export default function Page() {
} }
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
if (composer.blocked() || isChildSession()) return if (composer.blocked()) return
inputRef?.focus() inputRef?.focus()
} }
} }
@ -1121,10 +1127,7 @@ export default function Page() {
setFileTreeTab("all") setFileTreeTab("all")
} }
const focusInput = () => { const focusInput = () => inputRef?.focus()
if (isChildSession()) return
inputRef?.focus()
}
useSessionCommands({ useSessionCommands({
navigateMessageByOffset, navigateMessageByOffset,
@ -1149,6 +1152,7 @@ export default function Page() {
const label = (option: ChangeMode) => { const label = (option: ChangeMode) => {
if (option === "git") return language.t("ui.sessionReview.title.git") if (option === "git") return language.t("ui.sessionReview.title.git")
if (option === "branch") return language.t("ui.sessionReview.title.branch") if (option === "branch") return language.t("ui.sessionReview.title.branch")
if (option === "session") return language.t("ui.sessionReview.title")
return language.t("ui.sessionReview.title.lastTurn") return language.t("ui.sessionReview.title.lastTurn")
} }
@ -1171,26 +1175,11 @@ export default function Page() {
</div> </div>
) )
const createGit = (input: { emptyClass: string }) => (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
<div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
{language.t("session.review.noVcs.createGit.description")}
</div>
</div>
<Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
{gitMutation.isPending
? language.t("session.review.noVcs.createGit.actionLoading")
: language.t("session.review.noVcs.createGit.action")}
</Button>
</div>
)
const reviewEmptyText = createMemo(() => { const reviewEmptyText = createMemo(() => {
if (store.changes === "git") return language.t("session.review.noUncommittedChanges") if (store.changes === "git") return language.t("session.review.noUncommittedChanges")
if (store.changes === "branch") return language.t("session.review.noBranchChanges") if (store.changes === "branch") return language.t("session.review.noBranchChanges")
return language.t("session.review.noChanges") if (store.changes === "turn") return language.t("session.review.noChanges")
return language.t(sessionEmptyKey())
}) })
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => { const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
@ -1200,10 +1189,31 @@ export default function Page() {
} }
if (store.changes === "turn") { if (store.changes === "turn") {
if (nogit()) return createGit(input)
return empty(reviewEmptyText()) return empty(reviewEmptyText())
} }
if (hasSessionReview() && !diffsReady()) {
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
}
if (sessionEmptyKey() === "session.review.noVcs") {
return (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
<div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
{language.t("session.review.noVcs.createGit.description")}
</div>
</div>
<Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
{gitMutation.isPending
? language.t("session.review.noVcs.createGit.actionLoading")
: language.t("session.review.noVcs.createGit.action")}
</Button>
</div>
)
}
return ( return (
<div class={input.emptyClass}> <div class={input.emptyClass}>
<div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div> <div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
@ -1648,7 +1658,7 @@ export default function Page() {
const queueEnabled = createMemo(() => { const queueEnabled = createMemo(() => {
const id = params.id const id = params.id
if (!id) return false if (!id) return false
return settings.general.followup() === "queue" && busy(id) && !composer.blocked() && !isChildSession() return settings.general.followup() === "queue" && busy(id) && !composer.blocked()
}) })
const followupText = (item: FollowupDraft) => { const followupText = (item: FollowupDraft) => {
@ -1680,7 +1690,6 @@ export default function Page() {
const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) }))) const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) })))
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => { const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
if (sync.session.get(sessionID)?.parentID) return Promise.resolve()
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id) const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
if (!item) return Promise.resolve() if (!item) return Promise.resolve()
if (followupBusy(sessionID)) return Promise.resolve() if (followupBusy(sessionID)) return Promise.resolve()
@ -1811,7 +1820,6 @@ export default function Page() {
if (followupBusy(sessionID)) return if (followupBusy(sessionID)) return
if (followup.failed[sessionID] === item.id) return if (followup.failed[sessionID] === item.id) return
if (followup.paused[sessionID]) return if (followup.paused[sessionID]) return
if (isChildSession()) return
if (composer.blocked()) return if (composer.blocked()) return
if (busy(sessionID)) return if (busy(sessionID)) return
@ -1993,7 +2001,7 @@ export default function Page() {
}} }}
onResponseSubmit={resumeScroll} onResponseSubmit={resumeScroll}
followup={ followup={
params.id && !isChildSession() params.id
? { ? {
queue: queueEnabled, queue: queueEnabled,
items: followupDock(), items: followupDock(),

View File

@ -1,11 +1,9 @@
import { Show, createEffect, createMemo, onCleanup } from "solid-js" import { Show, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useNavigate } from "@solidjs/router"
import { useSpring } from "@opencode-ai/ui/motion-spring" import { useSpring } from "@opencode-ai/ui/motion-spring"
import { PromptInput } from "@/components/prompt-input" import { PromptInput } from "@/components/prompt-input"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
import { useSync } from "@/context/sync"
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
import { useSessionKey } from "@/pages/session/session-layout" import { useSessionKey } from "@/pages/session/session-layout"
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock" import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
@ -45,17 +43,11 @@ export function SessionComposerRegion(props: {
} }
setPromptDockRef: (el: HTMLDivElement) => void setPromptDockRef: (el: HTMLDivElement) => void
}) { }) {
const navigate = useNavigate()
const prompt = usePrompt() const prompt = usePrompt()
const language = useLanguage() const language = useLanguage()
const route = useSessionKey() const route = useSessionKey()
const sync = useSync()
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt) const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
const parentID = createMemo(() => info()?.parentID)
const child = createMemo(() => !!parentID())
const showComposer = createMemo(() => !props.state.blocked() || child())
const previewPrompt = () => const previewPrompt = () =>
prompt prompt
@ -121,12 +113,6 @@ export function SessionComposerRegion(props: {
const lift = createMemo(() => (rolled() ? 18 : 36 * value())) const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
const full = createMemo(() => Math.max(78, store.height)) const full = createMemo(() => Math.max(78, store.height))
const openParent = () => {
const id = parentID()
if (!id) return
navigate(`/${route.params.dir}/session/${id}`)
}
createEffect(() => { createEffect(() => {
const el = store.body const el = store.body
if (!el) return if (!el) return
@ -170,7 +156,7 @@ export function SessionComposerRegion(props: {
)} )}
</Show> </Show>
<Show when={showComposer()}> <Show when={!props.state.blocked()}>
<Show <Show
when={prompt.ready()} when={prompt.ready()}
fallback={ fallback={
@ -246,40 +232,17 @@ export function SessionComposerRegion(props: {
onEdit={props.followup!.onEdit} onEdit={props.followup!.onEdit}
/> />
</Show> </Show>
<Show <PromptInput
when={child()} ref={props.inputRef}
fallback={ newSessionWorktree={props.newSessionWorktree}
<Show when={!props.state.blocked()}> onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
<PromptInput edit={props.followup?.edit}
ref={props.inputRef} onEditLoaded={props.followup?.onEditLoaded}
newSessionWorktree={props.newSessionWorktree} shouldQueue={props.followup?.queue}
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset} onQueue={props.followup?.onQueue}
edit={props.followup?.edit} onAbort={props.followup?.onAbort}
onEditLoaded={props.followup?.onEditLoaded} onSubmit={props.onSubmit}
shouldQueue={props.followup?.queue} />
onQueue={props.followup?.onQueue}
onAbort={props.followup?.onAbort}
onSubmit={props.onSubmit}
/>
</Show>
}
>
<div
ref={props.inputRef}
class="w-full rounded-[12px] border border-border-weak-base bg-background-base p-3 text-16-regular text-text-weak"
>
<span>{language.t("session.child.promptDisabled")} </span>
<Show when={parentID()}>
<button
type="button"
class="text-text-base transition-colors hover:text-text-strong"
onClick={openParent}
>
{language.t("session.child.backToParent")}
</button>
</Show>
</div>
</Show>
</div> </div>
</Show> </Show>
</Show> </Show>

View File

@ -21,7 +21,6 @@ import { Popover as KobaltePopover } from "@kobalte/core/popover"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage" import { SessionContextUsage } from "@/components/session-context-usage"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSessionKey } from "@/pages/session/session-layout" import { useSessionKey } from "@/pages/session/session-layout"
import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSDK } from "@/context/global-sdk"
@ -69,16 +68,6 @@ const messageComments = (parts: Part[]): MessageComment[] =>
] ]
}) })
const taskDescription = (part: Part, sessionID: string) => {
if (part.type !== "tool" || part.tool !== "task") return
const metadata = "metadata" in part.state ? part.state.metadata : undefined
if (metadata?.sessionId !== sessionID) return
const value = part.state.input?.description
if (typeof value === "string" && value) return value
}
const pace = (width: number) => Math.round(Math.max(1200, Math.min(3200, (Math.max(width, 360) * 2000) / 900)))
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
const current = target instanceof Element ? target : undefined const current = target instanceof Element ? target : undefined
const nested = current?.closest("[data-scrollable]") const nested = current?.closest("[data-scrollable]")
@ -306,32 +295,6 @@ export function MessageTimeline(props: {
const shareUrl = createMemo(() => info()?.share?.url) const shareUrl = createMemo(() => info()?.share?.url)
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const parentID = createMemo(() => info()?.parentID) const parentID = createMemo(() => info()?.parentID)
const parent = createMemo(() => {
const id = parentID()
if (!id) return
return sync.session.get(id)
})
const parentMessages = createMemo(() => {
const id = parentID()
if (!id) return emptyMessages
return sync.data.message[id] ?? emptyMessages
})
const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
const childTaskDescription = createMemo(() => {
const id = sessionID()
if (!id) return
return parentMessages()
.flatMap((message) => sync.data.part[message.id] ?? [])
.map((part) => taskDescription(part, id))
.findLast((value): value is string => !!value)
})
const childTitle = createMemo(() => {
if (!parentID()) return titleLabel() ?? ""
if (childTaskDescription()) return childTaskDescription()
const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "")
if (value) return value
return language.t("command.session.new")
})
const showHeader = createMemo(() => !!(titleValue() || parentID())) const showHeader = createMemo(() => !!(titleValue() || parentID()))
const stageCfg = { init: 1, batch: 3 } const stageCfg = { init: 1, batch: 3 }
const staging = createTimelineStaging({ const staging = createTimelineStaging({
@ -354,20 +317,8 @@ export function MessageTimeline(props: {
open: false, open: false,
dismiss: null as "escape" | "outside" | null, dismiss: null as "escape" | "outside" | null,
}) })
const [bar, setBar] = createStore({
ms: pace(640),
})
let more: HTMLButtonElement | undefined let more: HTMLButtonElement | undefined
let head: HTMLDivElement | undefined
createResizeObserver(
() => head,
() => {
if (!head || head.clientWidth <= 0) return
setBar("ms", pace(head.clientWidth))
},
)
const viewShare = () => { const viewShare = () => {
const url = shareUrl() const url = shareUrl()
@ -447,20 +398,8 @@ export function MessageTimeline(props: {
), ),
) )
createEffect(
on(
() => [parentID(), childTaskDescription()] as const,
([id, description]) => {
if (!id || description) return
if (sync.data.message[id] !== undefined) return
void sync.session.sync(id)
},
{ defer: true },
),
)
const openTitleEditor = () => { const openTitleEditor = () => {
if (!sessionID() || parentID()) return if (!sessionID()) return
setTitle({ editing: true, draft: titleLabel() ?? "" }) setTitle({ editing: true, draft: titleLabel() ?? "" })
requestAnimationFrame(() => { requestAnimationFrame(() => {
titleRef?.focus() titleRef?.focus()
@ -707,53 +646,27 @@ export function MessageTimeline(props: {
<div ref={props.setContentRef} class="min-w-0 w-full"> <div ref={props.setContentRef} class="min-w-0 w-full">
<Show when={showHeader()}> <Show when={showHeader()}>
<div <div
ref={(el) => {
head = el
setBar("ms", pace(el.clientWidth))
}}
data-session-title data-session-title
classList={{ classList={{
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true, "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
relative: true,
"w-full": true, "w-full": true,
"pb-4": true, "pb-4": true,
"pl-2 pr-3 md:pl-4 md:pr-3": true, "pl-2 pr-3 md:pl-4 md:pr-3": true,
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}} }}
> >
<Show when={workingStatus() !== "hidden"}>
<div
data-component="session-progress"
data-state={workingStatus()}
aria-hidden="true"
style={{
"--session-progress-color": tint() ?? "var(--icon-interactive-base)",
"--session-progress-ms": `${bar.ms}ms`,
}}
>
<div data-component="session-progress-bar" />
</div>
</Show>
<div class="h-12 w-full flex items-center justify-between gap-2"> <div class="h-12 w-full flex items-center justify-between gap-2">
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3"> <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
<Show when={parentID()}>
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={navigateParent}
aria-label={language.t("common.goBack")}
/>
</Show>
<div class="flex items-center min-w-0 grow-1"> <div class="flex items-center min-w-0 grow-1">
<Show when={parentID()}>
<button
type="button"
data-slot="session-title-parent"
class="min-w-0 max-w-[40%] truncate text-14-medium text-text-weak transition-colors hover:text-text-base"
onClick={navigateParent}
>
{parentTitle()}
</button>
<span
data-slot="session-title-separator"
class="px-2 text-14-medium text-text-weak"
aria-hidden="true"
>
/
</span>
</Show>
<div <div
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]" class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
style={{ style={{
@ -771,16 +684,15 @@ export function MessageTimeline(props: {
</div> </div>
</Show> </Show>
</div> </div>
<Show when={childTitle() || title.editing}> <Show when={titleLabel() || title.editing}>
<Show <Show
when={title.editing} when={title.editing}
fallback={ fallback={
<h1 <h1
data-slot="session-title-child"
class="text-14-medium text-text-strong truncate grow-1 min-w-0" class="text-14-medium text-text-strong truncate grow-1 min-w-0"
onDblClick={openTitleEditor} onDblClick={openTitleEditor}
> >
{childTitle()} {titleLabel()}
</h1> </h1>
} }
> >
@ -788,7 +700,6 @@ export function MessageTimeline(props: {
ref={(el) => { ref={(el) => {
titleRef = el titleRef = el
}} }}
data-slot="session-title-child"
value={title.draft} value={title.draft}
disabled={titleMutation.isPending} disabled={titleMutation.isPending}
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]" class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
@ -816,179 +727,177 @@ export function MessageTimeline(props: {
{(id) => ( {(id) => (
<div class="shrink-0 flex items-center gap-3"> <div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" /> <SessionContextUsage placement="bottom" />
<Show when={!parentID()}> <DropdownMenu
<DropdownMenu gutter={4}
gutter={4} placement="bottom-end"
placement="bottom-end" open={title.menuOpen}
open={title.menuOpen} onOpenChange={(open) => {
onOpenChange={(open) => { setTitle("menuOpen", open)
setTitle("menuOpen", open) if (open) return
if (open) return }}
>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
classList={{
"bg-surface-base-active": share.open || title.pendingShare,
}} }}
> aria-label={language.t("common.moreOptions")}
<DropdownMenu.Trigger aria-expanded={title.menuOpen || share.open || title.pendingShare}
as={IconButton} ref={(el: HTMLButtonElement) => {
icon="dot-grid" more = el
variant="ghost" }}
class="size-6 rounded-md data-[expanded]:bg-surface-base-active" />
classList={{ <DropdownMenu.Portal>
"bg-surface-base-active": share.open || title.pendingShare, <DropdownMenu.Content
style={{ "min-width": "104px" }}
onCloseAutoFocus={(event) => {
if (title.pendingRename) {
event.preventDefault()
setTitle("pendingRename", false)
openTitleEditor()
return
}
if (title.pendingShare) {
event.preventDefault()
requestAnimationFrame(() => {
setShare({ open: true, dismiss: null })
setTitle("pendingShare", false)
})
}
}} }}
aria-label={language.t("common.moreOptions")} >
aria-expanded={title.menuOpen || share.open || title.pendingShare} <DropdownMenu.Item
ref={(el: HTMLButtonElement) => { onSelect={() => {
more = el setTitle("pendingRename", true)
}} setTitle("menuOpen", false)
/>
<DropdownMenu.Portal>
<DropdownMenu.Content
style={{ "min-width": "104px" }}
onCloseAutoFocus={(event) => {
if (title.pendingRename) {
event.preventDefault()
setTitle("pendingRename", false)
openTitleEditor()
return
}
if (title.pendingShare) {
event.preventDefault()
requestAnimationFrame(() => {
setShare({ open: true, dismiss: null })
setTitle("pendingShare", false)
})
}
}} }}
> >
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={shareEnabled()}>
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => { onSelect={() => {
setTitle("pendingRename", true) setTitle({ pendingShare: true, menuOpen: false })
setTitle("menuOpen", false)
}} }}
> >
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>
{language.t("session.share.action.share")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
<Show when={shareEnabled()}> </Show>
<DropdownMenu.Item <DropdownMenu.Item onSelect={() => void archiveSession(id())}>
onSelect={() => { <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
setTitle({ pendingShare: true, menuOpen: false }) </DropdownMenu.Item>
}} <DropdownMenu.Separator />
> <DropdownMenu.Item
<DropdownMenu.ItemLabel> onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
{language.t("session.share.action.share")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<KobaltePopover
open={share.open}
anchorRef={() => more}
placement="bottom-end"
gutter={4}
modal={false}
onOpenChange={(open) => {
if (open) setShare("dismiss", null)
setShare("open", open)
}}
>
<KobaltePopover.Portal>
<KobaltePopover.Content
data-component="popover-content"
style={{ "min-width": "320px" }}
onEscapeKeyDown={(event) => {
setShare({ dismiss: "escape", open: false })
event.preventDefault()
event.stopPropagation()
}}
onPointerDownOutside={() => {
setShare({ dismiss: "outside", open: false })
}}
onFocusOutside={() => {
setShare({ dismiss: "outside", open: false })
}}
onCloseAutoFocus={(event) => {
if (share.dismiss === "outside") event.preventDefault()
setShare("dismiss", null)
}}
> >
<div class="flex flex-col p-3"> <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
<div class="flex flex-col gap-1"> </DropdownMenu.Item>
<div class="text-13-medium text-text-strong"> </DropdownMenu.Content>
{language.t("session.share.popover.title")} </DropdownMenu.Portal>
</div> </DropdownMenu>
<div class="text-12-regular text-text-weak">
{shareUrl() <KobaltePopover
? language.t("session.share.popover.description.shared") open={share.open}
: language.t("session.share.popover.description.unshared")} anchorRef={() => more}
</div> placement="bottom-end"
gutter={4}
modal={false}
onOpenChange={(open) => {
if (open) setShare("dismiss", null)
setShare("open", open)
}}
>
<KobaltePopover.Portal>
<KobaltePopover.Content
data-component="popover-content"
style={{ "min-width": "320px" }}
onEscapeKeyDown={(event) => {
setShare({ dismiss: "escape", open: false })
event.preventDefault()
event.stopPropagation()
}}
onPointerDownOutside={() => {
setShare({ dismiss: "outside", open: false })
}}
onFocusOutside={() => {
setShare({ dismiss: "outside", open: false })
}}
onCloseAutoFocus={(event) => {
if (share.dismiss === "outside") event.preventDefault()
setShare("dismiss", null)
}}
>
<div class="flex flex-col p-3">
<div class="flex flex-col gap-1">
<div class="text-13-medium text-text-strong">
{language.t("session.share.popover.title")}
</div> </div>
<div class="mt-3 flex flex-col gap-2"> <div class="text-12-regular text-text-weak">
<Show {shareUrl()
when={shareUrl()} ? language.t("session.share.popover.description.shared")
fallback={ : language.t("session.share.popover.description.unshared")}
</div>
</div>
<div class="mt-3 flex flex-col gap-2">
<Show
when={shareUrl()}
fallback={
<Button
size="large"
variant="primary"
class="w-full"
onClick={shareSession}
disabled={shareMutation.isPending}
>
{shareMutation.isPending
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</Button>
}
>
<div class="flex flex-col gap-2">
<TextField
value={shareUrl() ?? ""}
readOnly
copyable
copyKind="link"
tabIndex={-1}
class="w-full"
/>
<div class="grid grid-cols-2 gap-2">
<Button
size="large"
variant="secondary"
class="w-full shadow-none border border-border-weak-base"
onClick={unshareSession}
disabled={unshareMutation.isPending}
>
{unshareMutation.isPending
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</Button>
<Button <Button
size="large" size="large"
variant="primary" variant="primary"
class="w-full" class="w-full"
onClick={shareSession} onClick={viewShare}
disabled={shareMutation.isPending} disabled={unshareMutation.isPending}
> >
{shareMutation.isPending {language.t("session.share.action.view")}
? language.t("session.share.action.publishing")
: language.t("session.share.action.publish")}
</Button> </Button>
}
>
<div class="flex flex-col gap-2">
<TextField
value={shareUrl() ?? ""}
readOnly
copyable
copyKind="link"
tabIndex={-1}
class="w-full"
/>
<div class="grid grid-cols-2 gap-2">
<Button
size="large"
variant="secondary"
class="w-full shadow-none border border-border-weak-base"
onClick={unshareSession}
disabled={unshareMutation.isPending}
>
{unshareMutation.isPending
? language.t("session.share.action.unpublishing")
: language.t("session.share.action.unpublish")}
</Button>
<Button
size="large"
variant="primary"
class="w-full"
onClick={viewShare}
disabled={unshareMutation.isPending}
>
{language.t("session.share.action.view")}
</Button>
</div>
</div> </div>
</Show> </div>
</div> </Show>
</div> </div>
</KobaltePopover.Content> </div>
</KobaltePopover.Portal> </KobaltePopover.Content>
</KobaltePopover> </KobaltePopover.Portal>
</Show> </KobaltePopover>
</div> </div>
)} )}
</Show> </Show>

View File

@ -1,6 +1,6 @@
import { createEffect, createSignal, onCleanup, type JSX } from "solid-js" import { createEffect, createSignal, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { FileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import type { import type {
SessionReviewCommentActions, SessionReviewCommentActions,
@ -14,12 +14,10 @@ import type { LineComment } from "@/context/comments"
export type DiffStyle = "unified" | "split" export type DiffStyle = "unified" | "split"
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
export interface SessionReviewTabProps { export interface SessionReviewTabProps {
title?: JSX.Element title?: JSX.Element
empty?: JSX.Element empty?: JSX.Element
diffs: () => ReviewDiff[] diffs: () => FileDiff[]
view: () => ReturnType<ReturnType<typeof useLayout>["view"]> view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
diffStyle: DiffStyle diffStyle: DiffStyle
onDiffStyleChange?: (style: DiffStyle) => void onDiffStyleChange?: (style: DiffStyle) => void

View File

@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import type { UserMessage } from "@opencode-ai/sdk/v2" import type { UserMessage } from "@opencode-ai/sdk/v2"
import { resetSessionModel, syncSessionModel } from "./session-model-helpers" import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
const message = (input?: { agent?: string; model?: UserMessage["model"] }) => const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
({ ({
id: "msg", id: "msg",
sessionID: "session", sessionID: "session",
@ -10,6 +10,7 @@ const message = (input?: { agent?: string; model?: UserMessage["model"] }) =>
time: { created: 1 }, time: { created: 1 },
agent: input?.agent ?? "build", agent: input?.agent ?? "build",
model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" }, model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" },
variant: input?.variant,
}) as UserMessage }) as UserMessage
describe("syncSessionModel", () => { describe("syncSessionModel", () => {
@ -25,12 +26,10 @@ describe("syncSessionModel", () => {
reset() {}, reset() {},
}, },
}, },
message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }), message({ variant: "high" }),
) )
expect(calls).toEqual([ expect(calls).toEqual([message({ variant: "high" })])
message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }),
])
}) })
}) })

View File

@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo" import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { FileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
@ -27,7 +27,7 @@ import { useSessionLayout } from "@/pages/session/session-layout"
export function SessionSidePanel(props: { export function SessionSidePanel(props: {
canReview: () => boolean canReview: () => boolean
diffs: () => (SnapshotFileDiff | VcsFileDiff)[] diffs: () => FileDiff[]
diffsReady: () => boolean diffsReady: () => boolean
empty: () => string empty: () => string
hasReview: () => boolean hasReview: () => boolean

View File

@ -5,30 +5,9 @@ const defaults: Record<string, string> = {
plan: "var(--icon-agent-plan-base)", plan: "var(--icon-agent-plan-base)",
} }
const palette = [
"var(--icon-agent-ask-base)",
"var(--icon-agent-build-base)",
"var(--icon-agent-docs-base)",
"var(--icon-agent-plan-base)",
"var(--syntax-info)",
"var(--syntax-success)",
"var(--syntax-warning)",
"var(--syntax-property)",
"var(--syntax-constant)",
"var(--text-diff-add-base)",
"var(--text-diff-delete-base)",
"var(--icon-warning-base)",
]
function tone(name: string) {
let hash = 0
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0
return palette[hash % palette.length]
}
export function agentColor(name: string, custom?: string) { export function agentColor(name: string, custom?: string) {
if (custom) return custom if (custom) return custom
return defaults[name] ?? defaults[name.toLowerCase()] ?? tone(name.toLowerCase()) return defaults[name] ?? defaults[name.toLowerCase()]
} }
export function messageAgentColor( export function messageAgentColor(

View File

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

View File

@ -9,8 +9,8 @@ export const config = {
github: { github: {
repoUrl: "https://github.com/anomalyco/opencode", repoUrl: "https://github.com/anomalyco/opencode",
starsFormatted: { starsFormatted: {
compact: "140K", compact: "120K",
full: "140,000", full: "120,000",
}, },
}, },
@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page) // Static stats (used on landing page)
stats: { stats: {
contributors: "850", contributors: "800",
commits: "11,000", commits: "10,000",
monthlyUsers: "6.5M", monthlyUsers: "5M",
}, },
} as const } as const

View File

@ -249,7 +249,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.meta.description": "go.meta.description":
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و MiniMax M2.5 وMiniMax M2.7.", "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و MiniMax M2.5 وMiniMax M2.7.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body": "go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@ -297,7 +297,7 @@ export const dict = {
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة", "go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
"go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4": "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7", "go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7",
"go.how.title": "كيف يعمل Go", "go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا", "go.how.step1.title": "أنشئ حسابًا",
@ -319,10 +319,10 @@ export const dict = {
"go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.", "go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.",
"go.faq.q2": "ما النماذج التي يتضمنها Go؟", "go.faq.q2": "ما النماذج التي يتضمنها Go؟",
"go.faq.a2": "go.faq.a2":
"يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7، مع حدود سخية ووصول موثوق.", "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7، مع حدود سخية ووصول موثوق.",
"go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3": "go.faq.a3":
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و MiniMax M2.5 وMiniMax M2.7.", "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و MiniMax M2.5 وMiniMax M2.7.",
"go.faq.q4": "كم تكلفة Go؟", "go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
@ -345,7 +345,7 @@ export const dict = {
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
"go.faq.a9": "go.faq.a9":
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",

View File

@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.meta.description": "go.meta.description":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.",
"go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.title": "Modelos de codificação de baixo custo para todos",
"go.hero.body": "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.", "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.",
@ -302,7 +302,7 @@ export const dict = {
"go.problem.item1": "Preço de assinatura de baixo custo", "go.problem.item1": "Preço de assinatura de baixo custo",
"go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item2": "Limites generosos e acesso confiável",
"go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item3": "Feito para o maior número possível de programadores",
"go.problem.item4": "Inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7", "go.problem.item4": "Inclui GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7",
"go.how.title": "Como o Go funciona", "go.how.title": "Como o Go funciona",
"go.how.body": "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.", "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
@ -326,10 +326,10 @@ export const dict = {
"Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.", "Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.",
"go.faq.q2": "Quais modelos o Go inclui?", "go.faq.q2": "Quais modelos o Go inclui?",
"go.faq.a2": "go.faq.a2":
"Go inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7, com limites generosos e acesso confiável.", "Go inclui GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7, com limites generosos e acesso confiável.",
"go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.q3": "O Go é o mesmo que o Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.",
"go.faq.q4": "Quanto custa o Go?", "go.faq.q4": "Quanto custa o Go?",
"go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.beforePricing": "O Go custa",
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
@ -353,7 +353,7 @@ export const dict = {
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
"go.faq.a9": "go.faq.a9":
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",

View File

@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.meta.description": "go.meta.description":
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.",
"go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.title": "Kodningsmodeller til lav pris for alle",
"go.hero.body": "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.", "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.",
@ -299,7 +299,7 @@ export const dict = {
"go.problem.item1": "Lavpris abonnementspriser", "go.problem.item1": "Lavpris abonnementspriser",
"go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item2": "Generøse grænser og pålidelig adgang",
"go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item3": "Bygget til så mange programmører som muligt",
"go.problem.item4": "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7", "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7",
"go.how.title": "Hvordan Go virker", "go.how.title": "Hvordan Go virker",
"go.how.body": "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 starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
@ -323,10 +323,10 @@ export const dict = {
"Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.", "Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.",
"go.faq.q2": "Hvilke modeller inkluderer Go?", "go.faq.q2": "Hvilke modeller inkluderer Go?",
"go.faq.a2": "go.faq.a2":
"Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7, med generøse grænser og pålidelig adgang.", "Go inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7, med generøse grænser og pålidelig adgang.",
"go.faq.q3": "Er Go det samme som Zen?", "go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.",
"go.faq.q4": "Hvad koster Go?", "go.faq.q4": "Hvad koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned", "go.faq.a4.p1.pricingLink": "$5 første måned",
@ -349,7 +349,7 @@ export const dict = {
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
"go.faq.a9": "go.faq.a9":
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",

View File

@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
"go.meta.description": "go.meta.description":
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7.",
"go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.title": "Kostengünstige Coding-Modelle für alle",
"go.hero.body": "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.", "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.",
@ -301,7 +301,7 @@ export const dict = {
"go.problem.item1": "Kostengünstiges Abonnement", "go.problem.item1": "Kostengünstiges Abonnement",
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "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.item3": "Für so viele Programmierer wie möglich gebaut",
"go.problem.item4": "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7", "go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7",
"go.how.title": "Wie Go funktioniert", "go.how.title": "Wie Go funktioniert",
"go.how.body": "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 beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
@ -325,10 +325,10 @@ export const dict = {
"Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.", "Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.",
"go.faq.q2": "Welche Modelle beinhaltet Go?", "go.faq.q2": "Welche Modelle beinhaltet Go?",
"go.faq.a2": "go.faq.a2":
"Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7, mit großzügigen Limits und zuverlässigem Zugang.", "Go beinhaltet GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7, mit großzügigen Limits und zuverlässigem Zugang.",
"go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.q3": "Ist Go dasselbe wie Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7.",
"go.faq.q4": "Wie viel kostet Go?", "go.faq.q4": "Wie viel kostet Go?",
"go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.beforePricing": "Go kostet",
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
@ -352,7 +352,7 @@ export const dict = {
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
"go.faq.a9": "go.faq.a9":
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",

View File

@ -248,7 +248,7 @@ export const dict = {
"go.title": "OpenCode Go | Low cost coding models for everyone", "go.title": "OpenCode Go | Low cost coding models for everyone",
"go.meta.description": "go.meta.description":
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7.", "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7.",
"go.hero.title": "Low cost coding models for everyone", "go.hero.title": "Low cost coding models for everyone",
"go.hero.body": "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.", "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.",
@ -295,7 +295,7 @@ export const dict = {
"go.problem.item1": "Low cost subscription pricing", "go.problem.item1": "Low cost subscription pricing",
"go.problem.item2": "Generous limits and reliable access", "go.problem.item2": "Generous limits and reliable access",
"go.problem.item3": "Built for as many programmers as possible", "go.problem.item3": "Built for as many programmers as possible",
"go.problem.item4": "Includes GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7", "go.problem.item4": "Includes GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7",
"go.how.title": "How Go works", "go.how.title": "How Go works",
"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.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.title": "Create an account",
@ -318,10 +318,10 @@ export const dict = {
"Go is a low-cost subscription that gives you reliable access to capable open-source models for agentic coding.", "Go is a low-cost subscription that gives you reliable access to capable open-source models for agentic coding.",
"go.faq.q2": "What models does Go include?", "go.faq.q2": "What models does Go include?",
"go.faq.a2": "go.faq.a2":
"Go includes GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7, with generous limits and reliable access.", "Go includes GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7, with generous limits and reliable access.",
"go.faq.q3": "Is Go the same as Zen?", "go.faq.q3": "Is Go the same as Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7.",
"go.faq.q4": "How much does Go cost?", "go.faq.q4": "How much does Go cost?",
"go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.beforePricing": "Go costs",
"go.faq.a4.p1.pricingLink": "$5 first month", "go.faq.a4.p1.pricingLink": "$5 first month",
@ -345,7 +345,7 @@ export const dict = {
"go.faq.q9": "What is the difference between free models and Go?", "go.faq.q9": "What is the difference between free models and Go?",
"go.faq.a9": "go.faq.a9":
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
"zen.api.error.modelNotSupported": "Model {{model}} not supported", "zen.api.error.modelNotSupported": "Model {{model}} not supported",

View File

@ -254,7 +254,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
"go.meta.description": "go.meta.description":
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7.",
"go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.title": "Modelos de programación de bajo coste para todos",
"go.hero.body": "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.", "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.",
@ -303,7 +303,7 @@ export const dict = {
"go.problem.item1": "Precios de suscripción de bajo coste", "go.problem.item1": "Precios de suscripción de bajo coste",
"go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item2": "Límites generosos y acceso fiable",
"go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item3": "Creado para tantos programadores como sea posible",
"go.problem.item4": "Incluye GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7", "go.problem.item4": "Incluye GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7",
"go.how.title": "Cómo funciona Go", "go.how.title": "Cómo funciona Go",
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo 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.title": "Crear una cuenta",
@ -326,10 +326,10 @@ export const dict = {
"Go es una suscripción de bajo coste que te da acceso fiable a modelos de código abierto capaces para programación agéntica.", "Go es una suscripción de bajo coste que te da acceso fiable a modelos de código abierto capaces para programación agéntica.",
"go.faq.q2": "¿Qué modelos incluye Go?", "go.faq.q2": "¿Qué modelos incluye Go?",
"go.faq.a2": "go.faq.a2":
"Go incluye GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7, con límites generosos y acceso fiable.", "Go incluye GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7, con límites generosos y acceso fiable.",
"go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.q3": "¿Es Go lo mismo que Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7.",
"go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.q4": "¿Cuánto cuesta Go?",
"go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.beforePricing": "Go cuesta",
"go.faq.a4.p1.pricingLink": "$5 el primer mes", "go.faq.a4.p1.pricingLink": "$5 el primer mes",
@ -353,7 +353,7 @@ export const dict = {
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
"go.faq.a9": "go.faq.a9":
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",

View File

@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
"go.meta.description": "go.meta.description":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7.",
"go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.title": "Modèles de code à faible coût pour tous",
"go.hero.body": "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é.", "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é.",
@ -303,7 +303,7 @@ export const dict = {
"go.problem.item1": "Prix d'abonnement bas", "go.problem.item1": "Prix d'abonnement bas",
"go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item2": "Limites généreuses et accès fiable",
"go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item3": "Conçu pour autant de programmeurs que possible",
"go.problem.item4": "Inclut GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7", "go.problem.item4": "Inclut GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7",
"go.how.title": "Comment fonctionne Go", "go.how.title": "Comment fonctionne Go",
"go.how.body": "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 commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
@ -327,10 +327,10 @@ export const dict = {
"Go est un abonnement à faible coût qui vous donne un accès fiable à des modèles open source performants pour le codage agentique.", "Go est un abonnement à faible coût qui vous donne un accès fiable à des modèles open source performants pour le codage agentique.",
"go.faq.q2": "Quels modèles Go inclut-il ?", "go.faq.q2": "Quels modèles Go inclut-il ?",
"go.faq.a2": "go.faq.a2":
"Go inclut GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7, avec des limites généreuses et un accès fiable.", "Go inclut GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7, 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.q3": "Est-ce que Go est la même chose que Zen ?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7.",
"go.faq.q4": "Combien coûte Go ?", "go.faq.q4": "Combien coûte Go ?",
"go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.beforePricing": "Go coûte",
"go.faq.a4.p1.pricingLink": "$5 le premier mois", "go.faq.a4.p1.pricingLink": "$5 le premier mois",
@ -353,7 +353,7 @@ export const dict = {
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
"go.faq.a9": "go.faq.a9":
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",

View File

@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
"go.meta.description": "go.meta.description":
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.",
"go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.title": "Modelli di coding a basso costo per tutti",
"go.hero.body": "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à.", "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à.",
@ -299,7 +299,7 @@ export const dict = {
"go.problem.item1": "Prezzo di abbonamento a basso costo", "go.problem.item1": "Prezzo di abbonamento a basso costo",
"go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item2": "Limiti generosi e accesso affidabile",
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
"go.problem.item4": "Include GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7", "go.problem.item4": "Include GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7",
"go.how.title": "Come funziona Go", "go.how.title": "Come funziona Go",
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo 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.title": "Crea un account",
@ -322,10 +322,10 @@ export const dict = {
"Go è un abbonamento a basso costo che ti dà un accesso affidabile a modelli open source capaci per il coding agentico.", "Go è un abbonamento a basso costo che ti dà un accesso affidabile a modelli open source capaci per il coding agentico.",
"go.faq.q2": "Quali modelli include Go?", "go.faq.q2": "Quali modelli include Go?",
"go.faq.a2": "go.faq.a2":
"Go include GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7, con limiti generosi e accesso affidabile.", "Go include GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7, con limiti generosi e accesso affidabile.",
"go.faq.q3": "Go è lo stesso di Zen?", "go.faq.q3": "Go è lo stesso di Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7.",
"go.faq.q4": "Quanto costa Go?", "go.faq.q4": "Quanto costa Go?",
"go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.beforePricing": "Go costa",
"go.faq.a4.p1.pricingLink": "$5 il primo mese", "go.faq.a4.p1.pricingLink": "$5 il primo mese",
@ -349,7 +349,7 @@ export const dict = {
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
"go.faq.a9": "go.faq.a9":
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato",

View File

@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.meta.description": "go.meta.description":
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。", "Goは最初の月$5、その後$10/月で、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。",
"go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.title": "すべての人のための低価格なコーディングモデル",
"go.hero.body": "go.hero.body":
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
@ -299,7 +299,7 @@ export const dict = {
"go.problem.item1": "低価格なサブスクリプション料金", "go.problem.item1": "低価格なサブスクリプション料金",
"go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item2": "十分な制限と安定したアクセス",
"go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item3": "できるだけ多くのプログラマーのために構築",
"go.problem.item4": "GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7を含む", "go.problem.item4": "GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7を含む",
"go.how.title": "Goの仕組み", "go.how.title": "Goの仕組み",
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
"go.how.step1.title": "アカウントを作成", "go.how.step1.title": "アカウントを作成",
@ -322,10 +322,10 @@ export const dict = {
"Goは、エージェント型コーディングのための有能なオープンソースモデルへの安定したアクセスを提供する低価格なサブスクリプションです。", "Goは、エージェント型コーディングのための有能なオープンソースモデルへの安定したアクセスを提供する低価格なサブスクリプションです。",
"go.faq.q2": "Goにはどのモデルが含まれますか", "go.faq.q2": "Goにはどのモデルが含まれますか",
"go.faq.a2": "go.faq.a2":
"Goには、GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7が含まれており、十分な制限と安定したアクセスが提供されます。", "Goには、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7が含まれており、十分な制限と安定したアクセスが提供されます。",
"go.faq.q3": "GoはZenと同じですか", "go.faq.q3": "GoはZenと同じですか",
"go.faq.a3": "go.faq.a3":
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
"go.faq.q4": "Goの料金は", "go.faq.q4": "Goの料金は",
"go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.beforePricing": "Goは",
"go.faq.a4.p1.pricingLink": "最初の月$5", "go.faq.a4.p1.pricingLink": "最初の月$5",
@ -349,7 +349,7 @@ export const dict = {
"go.faq.q9": "無料モデルとGoの違いは何ですか", "go.faq.q9": "無料モデルとGoの違いは何ですか",
"go.faq.a9": "go.faq.a9":
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ5時間、週間、月間全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です実際のリクエスト数はモデルと使用状況により異なります。", "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ5時間、週間、月間全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です実際のリクエスト数はモデルと使用状況により異なります。",
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",

View File

@ -247,7 +247,7 @@ export const dict = {
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.meta.description": "go.meta.description":
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body": "go.hero.body":
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
@ -296,7 +296,7 @@ export const dict = {
"go.problem.item1": "저렴한 구독 가격", "go.problem.item1": "저렴한 구독 가격",
"go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item2": "넉넉한 한도와 안정적인 액세스",
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
"go.problem.item4": "GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7 포함", "go.problem.item4": "GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7 포함",
"go.how.title": "Go 작동 방식", "go.how.title": "Go 작동 방식",
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
"go.how.step1.title": "계정 생성", "go.how.step1.title": "계정 생성",
@ -318,10 +318,10 @@ export const dict = {
"go.faq.a1": "Go는 에이전트 코딩을 위한 유능한 오픈 소스 모델에 대해 안정적인 액세스를 제공하는 저비용 구독입니다.", "go.faq.a1": "Go는 에이전트 코딩을 위한 유능한 오픈 소스 모델에 대해 안정적인 액세스를 제공하는 저비용 구독입니다.",
"go.faq.q2": "Go에는 어떤 모델이 포함되나요?", "go.faq.q2": "Go에는 어떤 모델이 포함되나요?",
"go.faq.a2": "go.faq.a2":
"Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7가 포함됩니다.", "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7가 포함됩니다.",
"go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.q3": "Go는 Zen과 같은가요?",
"go.faq.a3": "go.faq.a3":
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.q4": "Go 비용은 얼마인가요?",
"go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.beforePricing": "Go 비용은",
"go.faq.a4.p1.pricingLink": "첫 달 $5", "go.faq.a4.p1.pricingLink": "첫 달 $5",
@ -344,7 +344,7 @@ export const dict = {
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
"go.faq.a9": "go.faq.a9":
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",

View File

@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.title": "OpenCode Go | Rimelige kodemodeller for alle",
"go.meta.description": "go.meta.description":
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.",
"go.hero.title": "Rimelige kodemodeller for alle", "go.hero.title": "Rimelige kodemodeller for alle",
"go.hero.body": "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.", "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.",
@ -299,7 +299,7 @@ export const dict = {
"go.problem.item1": "Rimelig abonnementspris", "go.problem.item1": "Rimelig abonnementspris",
"go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item2": "Rause grenser og pålitelig tilgang",
"go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item3": "Bygget for så mange programmerere som mulig",
"go.problem.item4": "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7", "go.problem.item4": "Inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7",
"go.how.title": "Hvordan Go fungerer", "go.how.title": "Hvordan Go fungerer",
"go.how.body": "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 starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
@ -323,10 +323,10 @@ export const dict = {
"Go er et rimelig abonnement som gir deg pålitelig tilgang til kapable åpen kildekode-modeller for agent-koding.", "Go er et rimelig abonnement som gir deg pålitelig tilgang til kapable åpen kildekode-modeller for agent-koding.",
"go.faq.q2": "Hvilke modeller inkluderer Go?", "go.faq.q2": "Hvilke modeller inkluderer Go?",
"go.faq.a2": "go.faq.a2":
"Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7, med rause grenser og pålitelig tilgang.", "Go inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7, med rause grenser og pålitelig tilgang.",
"go.faq.q3": "Er Go det samme som Zen?", "go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7.",
"go.faq.q4": "Hva koster Go?", "go.faq.q4": "Hva koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned", "go.faq.a4.p1.pricingLink": "$5 første måned",
@ -350,7 +350,7 @@ export const dict = {
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
"go.faq.a9": "go.faq.a9":
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",

View File

@ -252,7 +252,7 @@ export const dict = {
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
"go.meta.description": "go.meta.description":
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7.",
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
"go.hero.body": "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ść.", "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ść.",
@ -300,7 +300,7 @@ export const dict = {
"go.problem.item1": "Niskokosztowa cena subskrypcji", "go.problem.item1": "Niskokosztowa cena subskrypcji",
"go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item2": "Hojne limity i niezawodny dostęp",
"go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item3": "Stworzony dla jak największej liczby programistów",
"go.problem.item4": "Zawiera GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7", "go.problem.item4": "Zawiera GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7",
"go.how.title": "Jak działa Go", "go.how.title": "Jak działa Go",
"go.how.body": "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 zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
@ -324,10 +324,10 @@ export const dict = {
"Go to niskokosztowa subskrypcja, która daje niezawodny dostęp do zdolnych modeli open source dla agentów kodujących.", "Go to niskokosztowa subskrypcja, która daje niezawodny dostęp do zdolnych modeli open source dla agentów kodujących.",
"go.faq.q2": "Jakie modele zawiera Go?", "go.faq.q2": "Jakie modele zawiera Go?",
"go.faq.a2": "go.faq.a2":
"Go zawiera GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7, z hojnymi limitami i niezawodnym dostępem.", "Go zawiera GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7, z hojnymi limitami i niezawodnym dostępem.",
"go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.q3": "Czy Go to to samo co Zen?",
"go.faq.a3": "go.faq.a3":
"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.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7.",
"go.faq.q4": "Ile kosztuje Go?", "go.faq.q4": "Ile kosztuje Go?",
"go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.beforePricing": "Go kosztuje",
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
@ -351,7 +351,7 @@ export const dict = {
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
"go.faq.a9": "go.faq.a9":
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",

View File

@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.meta.description": "go.meta.description":
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7.", "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7.",
"go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.title": "Недорогие модели для кодинга для всех",
"go.hero.body": "go.hero.body":
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
@ -304,7 +304,7 @@ export const dict = {
"go.problem.item1": "Недорогая подписка", "go.problem.item1": "Недорогая подписка",
"go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item2": "Щедрые лимиты и надежный доступ",
"go.problem.item3": "Создан для максимального числа программистов", "go.problem.item3": "Создан для максимального числа программистов",
"go.problem.item4": "Включает GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7", "go.problem.item4": "Включает GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7",
"go.how.title": "Как работает Go", "go.how.title": "Как работает Go",
"go.how.body": "go.how.body":
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
@ -328,10 +328,10 @@ export const dict = {
"Go — это недорогая подписка, дающая надежный доступ к мощным моделям с открытым исходным кодом для агентов-программистов.", "Go — это недорогая подписка, дающая надежный доступ к мощным моделям с открытым исходным кодом для агентов-программистов.",
"go.faq.q2": "Какие модели включает Go?", "go.faq.q2": "Какие модели включает Go?",
"go.faq.a2": "go.faq.a2":
"Go включает GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7, с щедрыми лимитами и надежным доступом.", "Go включает GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7, с щедрыми лимитами и надежным доступом.",
"go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.q3": "Go — это то же самое, что и Zen?",
"go.faq.a3": "go.faq.a3":
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7.", "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7.",
"go.faq.q4": "Сколько стоит Go?", "go.faq.q4": "Сколько стоит Go?",
"go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.beforePricing": "Go стоит",
"go.faq.a4.p1.pricingLink": "$5 за первый месяц", "go.faq.a4.p1.pricingLink": "$5 за первый месяц",
@ -355,7 +355,7 @@ export const dict = {
"go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.q9": "В чем разница между бесплатными моделями и Go?",
"go.faq.a9": "go.faq.a9":
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",

View File

@ -250,7 +250,7 @@ export const dict = {
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.meta.description": "go.meta.description":
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7", "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.hero.body": "go.hero.body":
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
@ -297,7 +297,7 @@ export const dict = {
"go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ", "go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ",
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
"go.problem.item4": "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7", "go.problem.item4": "รวมถึง GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7",
"go.how.title": "Go ทำงานอย่างไร", "go.how.title": "Go ทำงานอย่างไร",
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.step1.title": "สร้างบัญชี", "go.how.step1.title": "สร้างบัญชี",
@ -320,10 +320,10 @@ export const dict = {
"Go คือการสมัครสมาชิกราคาประหยัดที่ให้คุณเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสำหรับการเขียนโค้ดแบบเอเจนต์ได้อย่างน่าเชื่อถือ", "Go คือการสมัครสมาชิกราคาประหยัดที่ให้คุณเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสำหรับการเขียนโค้ดแบบเอเจนต์ได้อย่างน่าเชื่อถือ",
"go.faq.q2": "Go รวมโมเดลอะไรบ้าง?", "go.faq.q2": "Go รวมโมเดลอะไรบ้าง?",
"go.faq.a2": "go.faq.a2":
"Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "Go รวมถึง GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
"go.faq.a3": "go.faq.a3":
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้", "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้",
"go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.q4": "Go ราคาเท่าไหร่?",
"go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.beforePricing": "Go ราคา",
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
@ -346,7 +346,7 @@ export const dict = {
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
"go.faq.a9": "go.faq.a9":
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",

View File

@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
"go.meta.description": "go.meta.description":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.", "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.",
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
"go.hero.body": "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.", "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.",
@ -302,7 +302,7 @@ export const dict = {
"go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması", "go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması",
"go.problem.item2": "Cömert limitler ve güvenilir erişim", "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.item3": "Mümkün olduğunca çok programcı için geliştirildi",
"go.problem.item4": "GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 içerir", "go.problem.item4": "GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 içerir",
"go.how.title": "Go nasıl çalışır?", "go.how.title": "Go nasıl çalışır?",
"go.how.body": "go.how.body":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
@ -326,10 +326,10 @@ export const dict = {
"Go, ajan tabanlı kodlama için yetenekli açık kaynaklı modellere güvenilir erişim sağlayan düşük maliyetli bir aboneliktir.", "Go, ajan tabanlı kodlama için yetenekli açık kaynaklı modellere güvenilir erişim sağlayan düşük maliyetli bir aboneliktir.",
"go.faq.q2": "Go hangi modelleri içerir?", "go.faq.q2": "Go hangi modelleri içerir?",
"go.faq.a2": "go.faq.a2":
"Go, cömert limitler ve güvenilir erişim ile GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 modellerini içerir.", "Go, cömert limitler ve güvenilir erişim ile GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 modellerini içerir.",
"go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.q3": "Go, Zen ile aynı mı?",
"go.faq.a3": "go.faq.a3":
"Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "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, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
"go.faq.q4": "Go ne kadar?", "go.faq.q4": "Go ne kadar?",
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
"go.faq.a4.p1.pricingLink": "İlk ay $5", "go.faq.a4.p1.pricingLink": "İlk ay $5",
@ -353,7 +353,7 @@ export const dict = {
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
"go.faq.a9": "go.faq.a9":
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",

View File

@ -241,7 +241,7 @@ export const dict = {
"go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.meta.description": "go.meta.description":
"Go 首月 $5之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。", "Go 首月 $5之后 $10/月,提供对 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。",
"go.hero.title": "人人可用的低成本编程模型", "go.hero.title": "人人可用的低成本编程模型",
"go.hero.body": "go.hero.body":
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
@ -288,7 +288,7 @@ export const dict = {
"go.problem.item1": "低成本订阅定价", "go.problem.item1": "低成本订阅定价",
"go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item2": "充裕的限额和可靠的访问",
"go.problem.item3": "为尽可能多的程序员打造", "go.problem.item3": "为尽可能多的程序员打造",
"go.problem.item4": "包含 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7", "go.problem.item4": "包含 GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7",
"go.how.title": "Go 如何工作", "go.how.title": "Go 如何工作",
"go.how.body": "Go 起价为首月 $5之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.body": "Go 起价为首月 $5之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "创建账户", "go.how.step1.title": "创建账户",
@ -308,10 +308,10 @@ export const dict = {
"go.faq.a1": "Go 是一项低成本订阅服务,为您提供对强大的开源模型的可靠访问,用于代理编程。", "go.faq.a1": "Go 是一项低成本订阅服务,为您提供对强大的开源模型的可靠访问,用于代理编程。",
"go.faq.q2": "Go 包含哪些模型?", "go.faq.q2": "Go 包含哪些模型?",
"go.faq.a2": "go.faq.a2":
"Go 包含 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7,并提供充裕的限额和可靠的访问。", "Go 包含 GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7,并提供充裕的限额和可靠的访问。",
"go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.q3": "Go 和 Zen 一样吗?",
"go.faq.a3": "go.faq.a3":
"不。Zen 是按量付费,而 Go 首月 $5之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 等开源模型。", "不。Zen 是按量付费,而 Go 首月 $5之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 等开源模型。",
"go.faq.q4": "Go 多少钱?", "go.faq.q4": "Go 多少钱?",
"go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.beforePricing": "Go 费用为",
"go.faq.a4.p1.pricingLink": "首月 $5", "go.faq.a4.p1.pricingLink": "首月 $5",
@ -333,7 +333,7 @@ export const dict = {
"go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.q9": "免费模型和 Go 之间的区别是什么?",
"go.faq.a9": "go.faq.a9":
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7并在滚动窗口5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60实际请求计数因模型和使用情况而异。", "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, MiniMax M2.5 和 MiniMax M2.7并在滚动窗口5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60实际请求计数因模型和使用情况而异。",
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
"zen.api.error.modelNotSupported": "不支持模型 {{model}}", "zen.api.error.modelNotSupported": "不支持模型 {{model}}",

View File

@ -241,7 +241,7 @@ export const dict = {
"go.title": "OpenCode Go | 低成本全民編碼模型", "go.title": "OpenCode Go | 低成本全民編碼模型",
"go.meta.description": "go.meta.description":
"Go 首月 $5之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。", "Go 首月 $5之後 $10/月,提供對 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。",
"go.hero.title": "低成本全民編碼模型", "go.hero.title": "低成本全民編碼模型",
"go.hero.body": "go.hero.body":
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
@ -288,7 +288,7 @@ export const dict = {
"go.problem.item1": "低成本訂閱定價", "go.problem.item1": "低成本訂閱定價",
"go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item2": "寬裕的限額與穩定存取",
"go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item3": "專為盡可能多的程式設計師打造",
"go.problem.item4": "包含 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7", "go.problem.item4": "包含 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7",
"go.how.title": "Go 如何運作", "go.how.title": "Go 如何運作",
"go.how.body": "Go 起價為首月 $5之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.body": "Go 起價為首月 $5之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "建立帳號", "go.how.step1.title": "建立帳號",
@ -308,10 +308,10 @@ export const dict = {
"go.faq.a1": "Go 是一個低成本訂閱方案,讓你穩定存取強大的開源模型以進行代理編碼。", "go.faq.a1": "Go 是一個低成本訂閱方案,讓你穩定存取強大的開源模型以進行代理編碼。",
"go.faq.q2": "Go 包含哪些模型?", "go.faq.q2": "Go 包含哪些模型?",
"go.faq.a2": "go.faq.a2":
"Go 包含 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7,並提供寬裕的限額與穩定存取。", "Go 包含 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7,並提供寬裕的限額與穩定存取。",
"go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.q3": "Go 與 Zen 一樣嗎?",
"go.faq.a3": "go.faq.a3":
"不。Zen 是按量付費,而 Go 首月 $5之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 等開源模型。", "不。Zen 是按量付費,而 Go 首月 $5之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 和 MiniMax M2.7 等開源模型。",
"go.faq.q4": "Go 費用是多少?", "go.faq.q4": "Go 費用是多少?",
"go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.beforePricing": "Go 費用為",
"go.faq.a4.p1.pricingLink": "首月 $5", "go.faq.a4.p1.pricingLink": "首月 $5",
@ -333,7 +333,7 @@ export const dict = {
"go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.q9": "免費模型與 Go 有什麼區別?",
"go.faq.a9": "go.faq.a9":
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7並在滾動視窗5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60實際請求數因模型和使用情況而異。", "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5、Kimi K2.5、MiMo-V2-Pro、MiMo-V2-Omni、MiniMax M2.5 與 MiniMax M2.7並在滾動視窗5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60實際請求數因模型和使用情況而異。",
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
"zen.api.error.modelNotSupported": "不支援模型 {{model}}", "zen.api.error.modelNotSupported": "不支援模型 {{model}}",

View File

@ -45,16 +45,16 @@ function LimitsGraph(props: { href: string }) {
const free = 200 const free = 200
const models = [ const models = [
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" }, { id: "glm", name: "GLM-5", req: 1150, d: "120ms" },
{ id: "glm-5", name: "GLM-5", req: 1150, d: "120ms" },
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro", req: 1290, d: "150ms" },
{ id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" }, { id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
{ id: "mimo-v2-pro", name: "MiMo-V2-Pro", req: 1290, d: "150ms" },
{ id: "mimo-v2-omni", name: "MiMo-V2-Omni", req: 2150, d: "270ms" },
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 14000, d: "330ms" }, { id: "minimax-m2.7", name: "MiniMax M2.7", req: 14000, d: "330ms" },
{ id: "minimax-m2.5", name: "MiniMax M2.5", req: 20000, d: "360ms" }, { id: "minimax-m2.5", name: "MiniMax M2.5", req: 20000, d: "360ms" },
] ]
const w = 720 const w = 720
const h = 270 const h = 260
const left = 40 const left = 40
const right = 60 const right = 60
const top = 18 const top = 18

View File

@ -1,4 +1,3 @@
import type { Stripe } from "stripe"
import { Billing } from "@opencode-ai/console-core/billing.js" import { Billing } from "@opencode-ai/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js" import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
@ -112,17 +111,27 @@ export async function POST(input: APIEvent) {
const customerID = body.data.object.customer as string const customerID = body.data.object.customer as string
const invoiceID = body.data.object.latest_invoice as string const invoiceID = body.data.object.latest_invoice as string
const subscriptionID = body.data.object.id as string const subscriptionID = body.data.object.id as string
const paymentMethodID = body.data.object.default_payment_method as string
if (!workspaceID) throw new Error("Workspace ID not found") if (!workspaceID) throw new Error("Workspace ID not found")
if (!userID) throw new Error("User ID not found") if (!userID) throw new Error("User ID not found")
if (!customerID) throw new Error("Customer ID not found") if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found") if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found") if (!subscriptionID) throw new Error("Subscription ID not found")
if (!paymentMethodID) throw new Error("Payment method ID not found")
// get payment id from invoice
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["payments"],
})
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
if (!paymentID) throw new Error("Payment ID not found")
// get payment method for the payment intent // get payment method for the payment intent
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID) const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
await Actor.provide("system", { workspaceID }, async () => { await Actor.provide("system", { workspaceID }, async () => {
// look up current billing // look up current billing
const billing = await Billing.get() const billing = await Billing.get()
@ -191,18 +200,26 @@ export async function POST(input: APIEvent) {
const amountInCents = body.data.object.amount_paid const amountInCents = body.data.object.amount_paid
const customerID = body.data.object.customer as string const customerID = body.data.object.customer as string
const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
const productID = body.data.object.lines?.data[0].pricing?.price_details?.product as string
if (!customerID) throw new Error("Customer ID not found") if (!customerID) throw new Error("Customer ID not found")
if (!invoiceID) throw new Error("Invoice ID not found") if (!invoiceID) throw new Error("Invoice ID not found")
if (!subscriptionID) throw new Error("Subscription ID not found") if (!subscriptionID) throw new Error("Subscription ID not found")
// get coupon id from subscription // get coupon id from subscription
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, { const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, {
expand: ["discounts", "payments"], expand: ["discounts"],
}) })
const paymentID = invoice.payments?.data[0]?.payment.payment_intent as string const couponID =
const couponID = (invoice.discounts[0] as Stripe.Discount).coupon?.id as string typeof subscriptionData.discounts[0] === "string"
? subscriptionData.discounts[0]
: subscriptionData.discounts[0]?.coupon?.id
const productID = subscriptionData.items.data[0].price.product as string
// get payment id from invoice
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
expand: ["payments"],
})
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
if (!paymentID) { if (!paymentID) {
// payment id can be undefined when using coupon // payment id can be undefined when using coupon
if (!couponID) throw new Error("Payment ID not found") if (!couponID) throw new Error("Payment ID not found")

View File

@ -287,7 +287,6 @@ export function LiteSection() {
<ul data-slot="promo-models"> <ul data-slot="promo-models">
<li>Kimi K2.5</li> <li>Kimi K2.5</li>
<li>GLM-5</li> <li>GLM-5</li>
<li>GLM-5.1</li>
<li>Mimo-V2-Pro</li> <li>Mimo-V2-Pro</li>
<li>Mimo-V2-Omni</li> <li>Mimo-V2-Omni</li>
<li>MiniMax M2.5</li> <li>MiniMax M2.5</li>

View File

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

View File

@ -254,7 +254,7 @@ export namespace Billing {
const createSession = () => const createSession = () =>
Billing.stripe().checkout.sessions.create({ Billing.stripe().checkout.sessions.create({
mode: "subscription", mode: "subscription",
discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }], discounts: [{ coupon: LiteData.firstMonth50Coupon() }],
...(billing.customerID ...(billing.customerID
? { ? {
customer: billing.customerID, customer: billing.customerID,

View File

@ -11,11 +11,6 @@ export namespace LiteData {
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product) export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price) export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr) export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
export const firstMonthCoupon = fn(z.string(), (email) => { export const firstMonth50Coupon = fn(z.void(), () => Resource.ZEN_LITE_PRICE.firstMonth50Coupon)
const invitees = Resource.ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES.value.split(",")
return invitees.includes(email)
? Resource.ZEN_LITE_PRICE.firstMonth100Coupon
: Resource.ZEN_LITE_PRICE.firstMonth50Coupon
})
export const planName = fn(z.void(), () => "lite") export const planName = fn(z.void(), () => "lite")
} }

View File

@ -142,12 +142,7 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
"price": string "price": string
"priceInr": number "priceInr": number

View File

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

View File

@ -142,12 +142,7 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
"price": string "price": string
"priceInr": number "priceInr": number

View File

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

View File

@ -142,12 +142,7 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
"price": string "price": string
"priceInr": number "priceInr": number

View File

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"private": true, "private": true,
"version": "1.4.0", "version": "1.3.17",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/util/fn" import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife" import { iife } from "@opencode-ai/util/iife"
import z from "zod" import z from "zod"
@ -27,7 +27,7 @@ export namespace Share {
}), }),
z.object({ z.object({
type: z.literal("session_diff"), type: z.literal("session_diff"),
data: z.custom<SnapshotFileDiff[]>(), data: z.custom<FileDiff[]>(),
}), }),
z.object({ z.object({
type: z.literal("model"), type: z.literal("model"),

View File

@ -1,4 +1,4 @@
import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/ui/session-turn" import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context" import { DataProvider } from "@opencode-ai/ui/context"
@ -51,7 +51,7 @@ const getData = query(async (shareID) => {
shareID: string shareID: string
session: Session[] session: Session[]
session_diff: { session_diff: {
[sessionID: string]: SnapshotFileDiff[] [sessionID: string]: FileDiff[]
} }
session_status: { session_status: {
[sessionID: string]: SessionStatus [sessionID: string]: SessionStatus

View File

@ -142,12 +142,7 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
"price": string "price": string
"priceInr": number "priceInr": number

View File

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

View File

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

View File

@ -142,12 +142,7 @@ declare module "sst" {
"type": "sst.sst.Secret" "type": "sst.sst.Secret"
"value": string "value": string
} }
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_LITE_PRICE": { "ZEN_LITE_PRICE": {
"firstMonth100Coupon": string
"firstMonth50Coupon": string "firstMonth50Coupon": string
"price": string "price": string
"priceInr": number "priceInr": number

View File

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

View File

@ -1,4 +1,8 @@
# Keybindings vs. Keymappings # 2.0
What we would change if we could
## Keybindings vs. Keymappings
Make it `keymappings`, closer to neovim. Can be layered like `<leader>abc`. Commands don't define their binding, but have an id that a key can be mapped to like Make it `keymappings`, closer to neovim. Can be layered like `<leader>abc`. Commands don't define their binding, but have an id that a key can be mapped to like

View File

@ -1,136 +0,0 @@
# Message Shape
Problem:
- stored messages need enough data to replay and resume a session later
- prompt hooks often just want to append a synthetic user/assistant message
- today that means faking ids, timestamps, and request metadata
## Option 1: Two Message Shapes
Keep `User` / `Assistant` for stored history, but clean them up.
```ts
type User = {
role: "user"
time: { created: number }
request: {
agent: string
model: ModelRef
variant?: string
format?: OutputFormat
system?: string
tools?: Record<string, boolean>
}
}
type Assistant = {
role: "assistant"
run: { agent: string; model: ModelRef; path: { cwd: string; root: string } }
usage: { cost: number; tokens: Tokens }
result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
}
```
Add a separate transient `PromptMessage` for prompt surgery.
```ts
type PromptMessage = {
role: "user" | "assistant"
parts: PromptPart[]
}
```
Plugin hook example:
```ts
prompt.push({
role: "user",
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
})
```
Tradeoff: prompt hooks get easy lightweight messages, but there are now two message shapes.
## Option 2: Prompt Mutators
Keep `User` / `Assistant` as the stored history model.
Prompt hooks do not build messages directly. The runtime gives them prompt mutators.
```ts
type PromptEditor = {
append(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
prepend(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
appendTo(target: "last-user" | "last-assistant", parts: PromptPart[]): void
insertAfter(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
insertBefore(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
}
```
Plugin hook examples:
```ts
prompt.append({
role: "user",
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
})
```
```ts
prompt.appendTo("last-user", [{ type: "text", text: BUILD_SWITCH }])
```
Tradeoff: avoids a second full message type and avoids fake ids/timestamps, but moves more magic into the hook API.
## Option 3: Separate Turn State
Move execution settings out of `User` and into a separate turn/request object.
```ts
type Turn = {
id: string
request: {
agent: string
model: ModelRef
variant?: string
format?: OutputFormat
system?: string
tools?: Record<string, boolean>
}
}
type User = {
role: "user"
turnID: string
time: { created: number }
}
type Assistant = {
role: "assistant"
turnID: string
usage: { cost: number; tokens: Tokens }
result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
}
```
Examples:
```ts
const turn = {
request: {
agent: "build",
model: { providerID: "openai", modelID: "gpt-5" },
},
}
```
```ts
const msg = {
role: "user",
turnID: turn.id,
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
}
```
Tradeoff: stored messages get much smaller and cleaner, but replay now has to join messages with turn state and prompt hooks still need a way to pick which turn they belong to.

View File

@ -1,16 +1,9 @@
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect" import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
import { import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
FetchHttpClient,
HttpClient,
HttpClientError,
HttpClientRequest,
HttpClientResponse,
} from "effect/unstable/http"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { AccountRepo, type AccountRow } from "./repo" import { AccountRepo, type AccountRow } from "./repo"
import { normalizeServerUrl } from "./url"
import { import {
type AccountError, type AccountError,
AccessToken, AccessToken,
@ -19,7 +12,6 @@ import {
Info, Info,
RefreshToken, RefreshToken,
AccountServiceError, AccountServiceError,
AccountTransportError,
Login, Login,
Org, Org,
OrgID, OrgID,
@ -38,7 +30,6 @@ export {
type AccountError, type AccountError,
AccountRepoError, AccountRepoError,
AccountServiceError, AccountServiceError,
AccountTransportError,
AccessToken, AccessToken,
RefreshToken, RefreshToken,
DeviceCode, DeviceCode,
@ -141,27 +132,12 @@ const isTokenFresh = (tokenExpiry: number | null, now: number) =>
const mapAccountServiceError = const mapAccountServiceError =
(message = "Account service operation failed") => (message = "Account service operation failed") =>
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, AccountError, R> => <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, AccountServiceError, R> =>
effect.pipe(Effect.mapError((cause) => accountErrorFromCause(cause, message))) effect.pipe(
Effect.mapError((cause) =>
const accountErrorFromCause = (cause: unknown, message: string): AccountError => { cause instanceof AccountServiceError ? cause : new AccountServiceError({ message, cause }),
if (cause instanceof AccountServiceError || cause instanceof AccountTransportError) { ),
return cause )
}
if (HttpClientError.isHttpClientError(cause)) {
switch (cause.reason._tag) {
case "TransportError": {
return AccountTransportError.fromHttpClientError(cause.reason)
}
default: {
return new AccountServiceError({ message, cause })
}
}
}
return new AccountServiceError({ message, cause })
}
export namespace Account { export namespace Account {
export interface Interface { export interface Interface {
@ -370,9 +346,8 @@ export namespace Account {
}) })
const login = Effect.fn("Account.login")(function* (server: string) { const login = Effect.fn("Account.login")(function* (server: string) {
const normalizedServer = normalizeServerUrl(server)
const response = yield* executeEffectOk( const response = yield* executeEffectOk(
HttpClientRequest.post(`${normalizedServer}/auth/device/code`).pipe( HttpClientRequest.post(`${server}/auth/device/code`).pipe(
HttpClientRequest.acceptJson, HttpClientRequest.acceptJson,
HttpClientRequest.schemaBodyJson(ClientId)(new ClientId({ client_id: clientId })), HttpClientRequest.schemaBodyJson(ClientId)(new ClientId({ client_id: clientId })),
), ),
@ -384,8 +359,8 @@ export namespace Account {
return new Login({ return new Login({
code: parsed.device_code, code: parsed.device_code,
user: parsed.user_code, user: parsed.user_code,
url: `${normalizedServer}${parsed.verification_uri_complete}`, url: `${server}${parsed.verification_uri_complete}`,
server: normalizedServer, server,
expiry: parsed.expires_in, expiry: parsed.expires_in,
interval: parsed.interval, interval: parsed.interval,
}) })

View File

@ -4,7 +4,6 @@ import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema" import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url"
export type AccountRow = (typeof AccountTable)["$inferSelect"] export type AccountRow = (typeof AccountTable)["$inferSelect"]
@ -126,13 +125,11 @@ export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Ser
const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) => const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) =>
tx((db) => { tx((db) => {
const url = normalizeServerUrl(input.url)
db.insert(AccountTable) db.insert(AccountTable)
.values({ .values({
id: input.id, id: input.id,
email: input.email, email: input.email,
url, url: input.url,
access_token: input.accessToken, access_token: input.accessToken,
refresh_token: input.refreshToken, refresh_token: input.refreshToken,
token_expiry: input.expiry, token_expiry: input.expiry,
@ -141,7 +138,7 @@ export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Ser
target: AccountTable.id, target: AccountTable.id,
set: { set: {
email: input.email, email: input.email,
url, url: input.url,
access_token: input.accessToken, access_token: input.accessToken,
refresh_token: input.refreshToken, refresh_token: input.refreshToken,
token_expiry: input.expiry, token_expiry: input.expiry,

View File

@ -1,5 +1,4 @@
import { Schema } from "effect" import { Schema } from "effect"
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
import { withStatics } from "@/util/schema" import { withStatics } from "@/util/schema"
@ -61,34 +60,7 @@ export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceE
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect),
}) {} }) {}
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", { export type AccountError = AccountRepoError | AccountServiceError
method: Schema.String,
url: Schema.String,
description: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect),
}) {
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
return new AccountTransportError({
method: error.request.method,
url: error.request.url,
description: error.description,
cause: error.cause,
})
}
override get message(): string {
return [
`Could not reach ${this.method} ${this.url}.`,
`This failed before the server returned an HTTP response.`,
this.description,
`Check your network, proxy, or VPN configuration and try again.`,
]
.filter(Boolean)
.join("\n")
}
}
export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError
export class Login extends Schema.Class<Login>("Login")({ export class Login extends Schema.Class<Login>("Login")({
code: DeviceCode, code: DeviceCode,

View File

@ -1,8 +0,0 @@
export const normalizeServerUrl = (input: string): string => {
const url = new URL(input)
url.search = ""
url.hash = ""
const pathname = url.pathname.replace(/\/+$/, "")
return pathname.length === 0 ? url.origin : `${url.origin}${pathname}`
}

View File

@ -71,10 +71,7 @@ export const AgentCommand = cmd({
async function getAvailableTools(agent: Agent.Info) { async function getAvailableTools(agent: Agent.Info) {
const model = agent.model ?? (await Provider.defaultModel()) const model = agent.model ?? (await Provider.defaultModel())
return ToolRegistry.tools({ return ToolRegistry.tools(model, agent)
...model,
agent,
})
} }
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) { async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {

View File

@ -289,6 +289,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
toast, toast,
renderer, renderer,
}) })
onCleanup(() => {
api.dispose()
})
const [ready, setReady] = createSignal(false) const [ready, setReady] = createSignal(false)
TuiPluginRuntime.init(api) TuiPluginRuntime.init(api)
.catch((error) => { .catch((error) => {
@ -599,7 +602,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
{ {
title: "Switch model variant", title: "Switch model variant",
value: "variant.list", value: "variant.list",
keybind: "variant_list",
category: "Agent", category: "Agent",
hidden: local.model.variant.list().length === 0, hidden: local.model.variant.list().length === 0,
slash: { slash: {
@ -673,7 +675,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
category: "System", category: "System",
}, },
{ {
title: "Toggle theme mode", title: "Toggle Theme Mode",
value: "theme.switch_mode", value: "theme.switch_mode",
onSelect: (dialog) => { onSelect: (dialog) => {
setMode(mode() === "dark" ? "light" : "dark") setMode(mode() === "dark" ? "light" : "dark")
@ -682,7 +684,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
category: "System", category: "System",
}, },
{ {
title: locked() ? "Unlock theme mode" : "Lock theme mode", title: locked() ? "Unlock Theme Mode" : "Lock Theme Mode",
value: "theme.mode.lock", value: "theme.mode.lock",
onSelect: (dialog) => { onSelect: (dialog) => {
if (locked()) unlock() if (locked()) unlock()

View File

@ -8,6 +8,7 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { DialogVariant } from "./dialog-variant" import { DialogVariant } from "./dialog-variant"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import * as fuzzysort from "fuzzysort" import * as fuzzysort from "fuzzysort"
import { consoleManagedProviderLabel } from "@tui/util/provider-origin"
export function useConnected() { export function useConnected() {
const sync = useSync() const sync = useSync()
@ -46,7 +47,11 @@ export function DialogModel(props: { providerID?: string }) {
key: item, key: item,
value: { providerID: provider.id, modelID: model.id }, value: { providerID: provider.id, modelID: model.id },
title: model.name ?? item.modelID, title: model.name ?? item.modelID,
description: provider.name, description: consoleManagedProviderLabel(
sync.data.console_state.consoleManagedProviders,
provider.id,
provider.name,
),
category, category,
disabled: provider.id === "opencode" && model.id.includes("-nano"), disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
@ -84,7 +89,9 @@ export function DialogModel(props: { providerID?: string }) {
description: favorites.some((item) => item.providerID === provider.id && item.modelID === model) description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
? "(Favorite)" ? "(Favorite)"
: undefined, : undefined,
category: connected() ? provider.name : undefined, category: connected()
? consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, provider.id, provider.name)
: undefined,
disabled: provider.id === "opencode" && model.includes("-nano"), disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
onSelect() { onSelect() {
@ -135,7 +142,7 @@ export function DialogModel(props: { providerID?: string }) {
const title = createMemo(() => { const title = createMemo(() => {
const value = provider() const value = provider()
if (!value) return "Select model" if (!value) return "Select model"
return value.name return consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, value.id, value.name)
}) })
function onSelect(providerID: string, modelID: string) { function onSelect(providerID: string, modelID: string) {

View File

@ -13,7 +13,7 @@ import { DialogModel } from "./dialog-model"
import { useKeyboard } from "@opentui/solid" import { useKeyboard } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard" import { Clipboard } from "@tui/util/clipboard"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { isConsoleManagedProvider } from "@tui/util/provider-origin" import { CONSOLE_MANAGED_ICON, isConsoleManagedProvider } from "@tui/util/provider-origin"
const PROVIDER_PRIORITY: Record<string, number> = { const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0, opencode: 0,
@ -49,7 +49,11 @@ export function createDialogProviderOptions() {
}[provider.id], }[provider.id],
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined, footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
gutter: connected ? <text fg={theme.success}></text> : undefined, gutter: consoleManaged ? (
<text fg={theme.textMuted}>{CONSOLE_MANAGED_ICON}</text>
) : connected ? (
<text fg={theme.success}></text>
) : undefined,
async onSelect() { async onSelect() {
if (consoleManaged) return if (consoleManaged) return

View File

@ -23,7 +23,7 @@ import { useRenderer, type JSX } from "@opentui/solid"
import { Editor } from "@tui/util/editor" import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard" import { Clipboard } from "../../util/clipboard"
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, FilePart } from "@opencode-ai/sdk/v2"
import { TuiEvent } from "../../event" import { TuiEvent } from "../../event"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
@ -36,6 +36,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv" import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings" import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill" import { DialogSkill } from "../dialog-skill"
import { CONSOLE_MANAGED_ICON, consoleManagedProviderLabel } from "@tui/util/provider-origin"
export type PromptProps = { export type PromptProps = {
sessionID?: string sessionID?: string
@ -95,8 +96,15 @@ export function Prompt(props: PromptProps) {
const list = createMemo(() => props.placeholders?.normal ?? []) const list = createMemo(() => props.placeholders?.normal ?? [])
const shell = createMemo(() => props.placeholders?.shell ?? []) const shell = createMemo(() => props.placeholders?.shell ?? [])
const [auto, setAuto] = createSignal<AutocompleteRef>() const [auto, setAuto] = createSignal<AutocompleteRef>()
const currentProviderLabel = createMemo(() => local.model.parsed().provider) const activeOrgName = createMemo(() => sync.data.console_state.activeOrgName)
const hasRightContent = createMemo(() => Boolean(props.right)) const canSwitchOrgs = createMemo(() => sync.data.console_state.switchableOrgCount > 1)
const currentProviderLabel = createMemo(() => {
const current = local.model.current()
const provider = local.model.parsed().provider
if (!current) return provider
return consoleManagedProviderLabel(sync.data.console_state.consoleManagedProviders, current.providerID, provider)
})
const hasRightContent = createMemo(() => Boolean(props.right || activeOrgName()))
function promptModelWarning() { function promptModelWarning() {
toast.show({ toast.show({
@ -137,7 +145,7 @@ export function Prompt(props: PromptProps) {
if (!props.sessionID) return undefined if (!props.sessionID) return undefined
const messages = sync.data.message[props.sessionID] const messages = sync.data.message[props.sessionID]
if (!messages) return undefined if (!messages) return undefined
return messages.findLast((m): m is UserMessage => m.role === "user") return messages.findLast((m) => m.role === "user")
}) })
const usage = createMemo(() => { const usage = createMemo(() => {
@ -201,10 +209,8 @@ export function Prompt(props: PromptProps) {
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent) const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
if (msg.agent && isPrimaryAgent) { if (msg.agent && isPrimaryAgent) {
local.agent.set(msg.agent) local.agent.set(msg.agent)
if (msg.model) { if (msg.model) local.model.set(msg.model)
local.model.set(msg.model) if (msg.variant) local.model.variant.set(msg.variant)
local.model.variant.set(msg.model.variant)
}
} }
} }
}) })
@ -1112,6 +1118,17 @@ export function Prompt(props: PromptProps) {
<Show when={hasRightContent()}> <Show when={hasRightContent()}>
<box flexDirection="row" gap={1} alignItems="center"> <box flexDirection="row" gap={1} alignItems="center">
{props.right} {props.right}
<Show when={activeOrgName()}>
<text
fg={theme.textMuted}
onMouseUp={() => {
if (!canSwitchOrgs()) return
command.trigger("console.org.switch")
}}
>
{`${CONSOLE_MANAGED_ICON} ${activeOrgName()}`}
</text>
</Show>
</box> </box>
</Show> </Show>
</box> </box>
@ -1143,7 +1160,7 @@ export function Prompt(props: PromptProps) {
} }
/> />
</box> </box>
<box width="100%" flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}> <Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
<box <box
flexDirection="row" flexDirection="row"

View File

@ -4,7 +4,8 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js" import { batch, onCleanup, onMount } from "solid-js"
export type EventSource = { export type EventSource = {
subscribe: (directory: string | undefined, handler: (event: Event) => void) => Promise<() => void> on: (handler: (event: Event) => void) => () => void
setWorkspace?: (workspaceID?: string) => void
} }
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
@ -17,6 +18,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
events?: EventSource events?: EventSource
}) => { }) => {
const abort = new AbortController() const abort = new AbortController()
let workspaceID: string | undefined
let sse: AbortController | undefined let sse: AbortController | undefined
function createSDK() { function createSDK() {
@ -26,6 +28,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
directory: props.directory, directory: props.directory,
fetch: props.fetch, fetch: props.fetch,
headers: props.headers, headers: props.headers,
experimental_workspaceID: workspaceID,
}) })
} }
@ -87,9 +90,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
})().catch(() => {}) })().catch(() => {})
} }
onMount(async () => { onMount(() => {
if (props.events) { if (props.events) {
const unsub = await props.events.subscribe(props.directory, handleEvent) const unsub = props.events.on(handleEvent)
onCleanup(unsub) onCleanup(unsub)
} else { } else {
startSSE() startSSE()
@ -106,9 +109,19 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
get client() { get client() {
return sdk return sdk
}, },
get workspaceID() {
return workspaceID
},
directory: props.directory, directory: props.directory,
event: emitter, event: emitter,
fetch: props.fetch ?? fetch, fetch: props.fetch ?? fetch,
setWorkspace(next?: string) {
if (workspaceID === next) return
workspaceID = next
sdk = createSDK()
props.events?.setWorkspace?.(next)
if (!props.events) startSSE()
},
url: props.url, url: props.url,
} }
}, },

View File

@ -18,7 +18,7 @@ import { Prompt } from "../component/prompt"
import { Slot as HostSlot } from "./slots" import { Slot as HostSlot } from "./slots"
import type { useToast } from "../ui/toast" import type { useToast } from "../ui/toast"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { type OpencodeClient } from "@opencode-ai/sdk/v2" import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
type RouteEntry = { type RouteEntry = {
key: symbol key: symbol
@ -43,6 +43,11 @@ type Input = {
renderer: TuiPluginApi["renderer"] renderer: TuiPluginApi["renderer"]
} }
type TuiHostPluginApi = TuiPluginApi & {
map: Map<string | undefined, OpencodeClient>
dispose: () => void
}
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) { function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
const key = Symbol() const key = Symbol()
for (const item of list) { for (const item of list) {
@ -201,7 +206,29 @@ function appApi(): TuiPluginApi["app"] {
} }
} }
export function createTuiApi(input: Input): TuiPluginApi { export function createTuiApi(input: Input): TuiHostPluginApi {
const map = new Map<string | undefined, OpencodeClient>()
const scoped: TuiPluginApi["scopedClient"] = (workspaceID) => {
const hit = map.get(workspaceID)
if (hit) return hit
const next = createOpencodeClient({
baseUrl: input.sdk.url,
fetch: input.sdk.fetch,
directory: input.sync.data.path.directory || input.sdk.directory,
experimental_workspaceID: workspaceID,
})
map.set(workspaceID, next)
return next
}
const workspace: TuiPluginApi["workspace"] = {
current() {
return input.sdk.workspaceID
},
set(workspaceID) {
input.sdk.setWorkspace(workspaceID)
},
}
const lifecycle: TuiPluginApi["lifecycle"] = { const lifecycle: TuiPluginApi["lifecycle"] = {
signal: new AbortController().signal, signal: new AbortController().signal,
onDispose() { onDispose() {
@ -342,6 +369,8 @@ export function createTuiApi(input: Input): TuiPluginApi {
get client() { get client() {
return input.sdk.client return input.sdk.client
}, },
scopedClient: scoped,
workspace,
event: input.sdk.event, event: input.sdk.event,
renderer: input.renderer, renderer: input.renderer,
slots: { slots: {
@ -393,5 +422,9 @@ export function createTuiApi(input: Input): TuiPluginApi {
return input.theme.ready return input.theme.ready
}, },
}, },
map,
dispose() {
map.clear()
},
} }
} }

View File

@ -543,6 +543,8 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
get client() { get client() {
return api.client return api.client
}, },
scopedClient: api.scopedClient,
workspace: api.workspace,
event, event,
renderer: api.renderer, renderer: api.renderer,
slots, slots,

View File

@ -167,6 +167,12 @@ export function Session() {
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
createEffect(() => {
if (session()?.workspaceID) {
sdk.setWorkspace(session()?.workspaceID)
}
})
createEffect(async () => { createEffect(async () => {
await sync.session await sync.session
.sync(route.sessionID) .sync(route.sessionID)
@ -2124,7 +2130,7 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
</text> </text>
} }
> >
<Diff diff={file.patch} filePath={file.filePath} /> <Diff diff={file.diff} filePath={file.filePath} />
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={file.movePath ?? file.filePath} /> <Diagnostics diagnostics={props.metadata.diagnostics} filePath={file.movePath ?? file.filePath} />
</Show> </Show>
</BlockTool> </BlockTool>

View File

@ -43,18 +43,9 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
function createEventSource(client: RpcClient): EventSource { function createEventSource(client: RpcClient): EventSource {
return { return {
subscribe: async (directory, handler) => { on: (handler) => client.on<Event>("event", handler),
const id = await client.call("subscribe", { directory }) setWorkspace: (workspaceID) => {
const unsub = client.on<{ id: string; event: Event }>("event", (e) => { void client.call("setWorkspace", { workspaceID })
if (e.id === id) {
handler(e.event)
}
})
return () => {
unsub()
client.call("unsubscribe", { id })
}
}, },
} }
} }

View File

@ -1,3 +1,5 @@
export const CONSOLE_MANAGED_ICON = "⌂"
const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) => const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
Array.isArray(consoleManagedProviders) Array.isArray(consoleManagedProviders)
? consoleManagedProviders.includes(providerID) ? consoleManagedProviders.includes(providerID)
@ -5,3 +7,14 @@ const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, provi
export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) => export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
contains(consoleManagedProviders, providerID) contains(consoleManagedProviders, providerID)
export const consoleManagedProviderSuffix = (
consoleManagedProviders: string[] | ReadonlySet<string>,
providerID: string,
) => (contains(consoleManagedProviders, providerID) ? ` ${CONSOLE_MANAGED_ICON}` : "")
export const consoleManagedProviderLabel = (
consoleManagedProviders: string[] | ReadonlySet<string>,
providerID: string,
providerName: string,
) => `${providerName}${consoleManagedProviderSuffix(consoleManagedProviders, providerID)}`

View File

@ -45,20 +45,20 @@ GlobalBus.on("event", (event) => {
let server: Awaited<ReturnType<typeof Server.listen>> | undefined let server: Awaited<ReturnType<typeof Server.listen>> | undefined
const eventStreams = new Map<string, AbortController>() const eventStream = {
abort: undefined as AbortController | undefined,
function startEventStream(directory: string) { }
const id = crypto.randomUUID()
const startEventStream = (input: { directory: string; workspaceID?: string }) => {
if (eventStream.abort) eventStream.abort.abort()
const abort = new AbortController() const abort = new AbortController()
eventStream.abort = abort
const signal = abort.signal const signal = abort.signal
eventStreams.set(id, abort) ;(async () => {
async function run() {
while (!signal.aborted) { while (!signal.aborted) {
const shouldReconnect = await Instance.provide({ const shouldReconnect = await Instance.provide({
directory, directory: input.directory,
init: InstanceBootstrap, init: InstanceBootstrap,
fn: () => fn: () =>
new Promise<boolean>((resolve) => { new Promise<boolean>((resolve) => {
@ -77,10 +77,7 @@ function startEventStream(directory: string) {
} }
const unsub = Bus.subscribeAll((event) => { const unsub = Bus.subscribeAll((event) => {
Rpc.emit("event", { Rpc.emit("event", event as Event)
id,
event: event as Event,
})
if (event.type === Bus.InstanceDisposed.type) { if (event.type === Bus.InstanceDisposed.type) {
settle(true) settle(true)
} }
@ -107,24 +104,14 @@ function startEventStream(directory: string) {
await sleep(250) await sleep(250)
} }
} }
} })().catch((error) => {
run().catch((error) => {
Log.Default.error("event stream error", { Log.Default.error("event stream error", {
error: error instanceof Error ? error.message : error, error: error instanceof Error ? error.message : error,
}) })
}) })
return id
} }
function stopEventStream(id: string) { startEventStream({ directory: process.cwd() })
const abortController = eventStreams.get(id)
if (!abortController) return
abortController.abort()
eventStreams.delete(id)
}
export const rpc = { export const rpc = {
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) { async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
@ -167,19 +154,12 @@ export const rpc = {
async reload() { async reload() {
await Config.invalidate(true) await Config.invalidate(true)
}, },
async subscribe(input: { directory: string | undefined }) { async setWorkspace(input: { workspaceID?: string }) {
return startEventStream(input.directory || process.cwd()) startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID })
},
async unsubscribe(input: { id: string }) {
stopEventStream(input.id)
}, },
async shutdown() { async shutdown() {
Log.Default.info("worker shutting down") Log.Default.info("worker shutting down")
if (eventStream.abort) eventStream.abort.abort()
for (const id of [...eventStreams.keys()]) {
stopEventStream(id)
}
await Instance.disposeAll() await Instance.disposeAll()
if (server) await server.stop(true) if (server) await server.stop(true)
}, },

View File

@ -1,4 +1,3 @@
import { AccountServiceError, AccountTransportError } from "@/account"
import { ConfigMarkdown } from "@/config/markdown" import { ConfigMarkdown } from "@/config/markdown"
import { errorFormat } from "@/util/error" import { errorFormat } from "@/util/error"
import { Config } from "../config/config" import { Config } from "../config/config"
@ -9,9 +8,6 @@ import { UI } from "./ui"
export function FormatError(input: unknown) { export function FormatError(input: unknown) {
if (MCP.Failed.isInstance(input)) if (MCP.Failed.isInstance(input))
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.` return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (input instanceof AccountTransportError || input instanceof AccountServiceError) {
return input.message
}
if (Provider.ModelNotFoundError.isInstance(input)) { if (Provider.ModelNotFoundError.isInstance(input)) {
const { providerID, modelID, suggestions } = input.data const { providerID, modelID, suggestions } = input.data
return [ return [

View File

@ -669,7 +669,6 @@ export namespace Config {
agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
variant_list: z.string().optional().default("none").describe("List model variants"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"), input_submit: z.string().optional().default("return").describe("Submit input"),

View File

@ -1,34 +0,0 @@
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Otlp } from "effect/unstable/observability"
import { Flag } from "@/flag/flag"
import { CHANNEL, VERSION } from "@/installation/meta"
export namespace Observability {
export const enabled = !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT
export const layer = !Flag.OTEL_EXPORTER_OTLP_ENDPOINT
? Layer.empty
: Otlp.layerJson({
baseUrl: Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
loggerMergeWithExisting: false,
resource: {
serviceName: "opencode",
serviceVersion: VERSION,
attributes: {
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
"opencode.client": Flag.OPENCODE_CLIENT,
},
},
headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
(acc, x) => {
const [key, value] = x.split("=")
acc[key] = value
return acc
},
{} as Record<string, string>,
)
: undefined,
}).pipe(Layer.provide(FetchHttpClient.layer))
}

View File

@ -3,7 +3,6 @@ import * as ServiceMap from "effect/ServiceMap"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Context } from "@/util/context" import { Context } from "@/util/context"
import { InstanceRef } from "./instance-ref" import { InstanceRef } from "./instance-ref"
import { Observability } from "./oltp"
export const memoMap = Layer.makeMemoMapUnsafe() export const memoMap = Layer.makeMemoMapUnsafe()
@ -19,7 +18,7 @@ function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) { export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) {
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap })) const getRuntime = () => (rt ??= ManagedRuntime.make(layer, { memoMap }))
return { return {
runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runSync(attach(service.use(fn))), runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runSync(attach(service.use(fn))),

View File

@ -11,9 +11,6 @@ function falsy(key: string) {
} }
export namespace Flag { export namespace Flag {
export const OTEL_EXPORTER_OTLP_ENDPOINT = process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]
export const OTEL_EXPORTER_OTLP_HEADERS = process.env["OTEL_EXPORTER_OTLP_HEADERS"]
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_AUTO_HEAP_SNAPSHOT = truthy("OPENCODE_AUTO_HEAP_SNAPSHOT") export const OPENCODE_AUTO_HEAP_SNAPSHOT = truthy("OPENCODE_AUTO_HEAP_SNAPSHOT")
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]

View File

@ -1,5 +1,4 @@
import { Effect, Layer, ServiceMap, Stream } from "effect" import { Effect, Layer, ServiceMap, Stream } from "effect"
import { formatPatch, structuredPatch } from "diff"
import path from "path" import path from "path"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
@ -8,6 +7,7 @@ import { makeRuntime } from "@/effect/run-service"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file/watcher"
import { Git } from "@/git" import { Git } from "@/git"
import { Snapshot } from "@/snapshot"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { Instance } from "./instance" import { Instance } from "./instance"
import z from "zod" import z from "zod"
@ -49,8 +49,6 @@ export namespace Vcs {
map: Map<string, { additions: number; deletions: number }>, map: Map<string, { additions: number; deletions: number }>,
) { ) {
const base = ref ? yield* git.prefix(cwd) : "" const base = ref ? yield* git.prefix(cwd) : ""
const patch = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
const next = yield* Effect.forEach( const next = yield* Effect.forEach(
list, list,
(item) => (item) =>
@ -60,11 +58,12 @@ export namespace Vcs {
const stat = map.get(item.file) const stat = map.get(item.file)
return { return {
file: item.file, file: item.file,
patch: patch(item.file, before, after), before,
after,
additions: stat?.additions ?? (item.status === "added" ? count(after) : 0), additions: stat?.additions ?? (item.status === "added" ? count(after) : 0),
deletions: stat?.deletions ?? (item.status === "deleted" ? count(before) : 0), deletions: stat?.deletions ?? (item.status === "deleted" ? count(before) : 0),
status: item.status, status: item.status,
} satisfies FileDiff } satisfies Snapshot.FileDiff
}), }),
{ concurrency: 8 }, { concurrency: 8 },
) )
@ -126,24 +125,11 @@ export namespace Vcs {
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const FileDiff = z
.object({
file: z.string(),
patch: z.string(),
additions: z.number(),
deletions: z.number(),
status: z.enum(["added", "deleted", "modified"]).optional(),
})
.meta({
ref: "VcsFileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>
export interface Interface { export interface Interface {
readonly init: () => Effect.Effect<void> readonly init: () => Effect.Effect<void>
readonly branch: () => Effect.Effect<string | undefined> readonly branch: () => Effect.Effect<string | undefined>
readonly defaultBranch: () => Effect.Effect<string | undefined> readonly defaultBranch: () => Effect.Effect<string | undefined>
readonly diff: (mode: Mode) => Effect.Effect<FileDiff[]> readonly diff: (mode: Mode) => Effect.Effect<Snapshot.FileDiff[]>
} }
interface State { interface State {

View File

@ -154,7 +154,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
description: "VCS diff", description: "VCS diff",
content: { content: {
"application/json": { "application/json": {
schema: resolver(Vcs.FileDiff.array()), schema: resolver(Snapshot.FileDiff.array()),
}, },
}, },
}, },

View File

@ -15,7 +15,6 @@ import { zodToJsonSchema } from "zod-to-json-schema"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { WorkspaceRoutes } from "./workspace" import { WorkspaceRoutes } from "./workspace"
import { Agent } from "@/agent/agent"
const ConsoleOrgOption = z.object({ const ConsoleOrgOption = z.object({
accountID: z.string(), accountID: z.string(),
@ -182,11 +181,7 @@ export const ExperimentalRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const { provider, model } = c.req.valid("query") const { provider, model } = c.req.valid("query")
const tools = await ToolRegistry.tools({ const tools = await ToolRegistry.tools({ providerID: ProviderID.make(provider), modelID: ModelID.make(model) })
providerID: ProviderID.make(provider),
modelID: ModelID.make(model),
agent: await Agent.get(await Agent.defaultAgent()),
})
return c.json( return c.json(
tools.map((t) => ({ tools.map((t) => ({
id: t.id, id: t.id,

View File

@ -228,7 +228,7 @@ When constructing the summary, try to stick to this template:
sessionID: input.sessionID, sessionID: input.sessionID,
mode: "compaction", mode: "compaction",
agent: "compaction", agent: "compaction",
variant: userMessage.model.variant, variant: userMessage.variant,
summary: true, summary: true,
path: { path: {
cwd: ctx.directory, cwd: ctx.directory,
@ -295,6 +295,7 @@ When constructing the summary, try to stick to this template:
format: original.format, format: original.format,
tools: original.tools, tools: original.tools,
system: original.system, system: original.system,
variant: original.variant,
}) })
for (const part of replay.parts) { for (const part of replay.parts) {
if (part.type === "compaction") continue if (part.type === "compaction") continue

View File

@ -127,9 +127,7 @@ export namespace LLM {
} }
const variant = const variant =
!input.small && input.model.variants && input.user.model.variant !input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {}
? input.model.variants[input.user.model.variant]
: {}
const base = input.small const base = input.small
? ProviderTransform.smallOptions(input.model) ? ProviderTransform.smallOptions(input.model)
: ProviderTransform.options({ : ProviderTransform.options({

View File

@ -371,10 +371,10 @@ export namespace MessageV2 {
model: z.object({ model: z.object({
providerID: ProviderID.zod, providerID: ProviderID.zod,
modelID: ModelID.zod, modelID: ModelID.zod,
variant: z.string().optional(),
}), }),
system: z.string().optional(), system: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(), tools: z.record(z.string(), z.boolean()).optional(),
variant: z.string().optional(),
}).meta({ }).meta({
ref: "UserMessage", ref: "UserMessage",
}) })

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