Merge remote-tracking branch 'upstream/dev' into desktop-wsl-onboarding
# Conflicts: # packages/desktop/src/main/apps.ts # packages/desktop/src/main/index.ts # packages/desktop/src/main/ipc.ts # packages/desktop/src/main/server.ts
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-v3.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.39",
|
||||
"version": "1.15.5",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -73,7 +73,6 @@
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"virtua": "catalog:",
|
||||
"zod": "catalog:"
|
||||
"virtua": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
document.documentElement.dataset.theme = themeId
|
||||
document.documentElement.dataset.colorScheme = mode
|
||||
|
||||
// Update theme-color meta tag to match app color scheme
|
||||
var metas = document.querySelectorAll("meta[name='theme-color']")
|
||||
if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#131010" : "#F8F7F7")
|
||||
|
||||
if (themeId === "oc-2") return
|
||||
|
||||
var css = localStorage.getItem("opencode-theme-css-" + mode)
|
||||
|
||||
@@ -107,7 +107,8 @@ function createCommandEntries(props: {
|
||||
const allowed = createMemo(() => {
|
||||
if (props.filesOnly()) return []
|
||||
return props.command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
(option) =>
|
||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { mcpQueryKey } from "@/context/global-sync"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const statusLabels = {
|
||||
connected: "mcp.status.connected",
|
||||
failed: "mcp.status.failed",
|
||||
needs_auth: "mcp.status.needs_auth",
|
||||
needs_client_registration: "mcp.status.needs_client_registration",
|
||||
disabled: "mcp.status.disabled",
|
||||
} as const
|
||||
|
||||
@@ -20,6 +22,7 @@ export const DialogSelectMcp: Component = () => {
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
const queryClient = useQueryClient()
|
||||
const queryOptions = useQueryOptions()
|
||||
|
||||
const items = createMemo(() =>
|
||||
Object.entries(sync.data.mcp ?? {})
|
||||
@@ -29,10 +32,18 @@ export const DialogSelectMcp: Component = () => {
|
||||
|
||||
const toggle = useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
||||
else await sdk.client.mcp.connect({ name })
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
return
|
||||
}
|
||||
if (status?.status === "needs_auth") {
|
||||
await sdk.client.mcp.auth.authenticate({ name })
|
||||
return
|
||||
}
|
||||
await sdk.client.mcp.connect({ name })
|
||||
},
|
||||
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
||||
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
||||
}))
|
||||
|
||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||
@@ -65,7 +76,7 @@ export const DialogSelectMcp: Component = () => {
|
||||
}
|
||||
const error = () => {
|
||||
const s = mcpStatus()
|
||||
return s?.status === "failed" ? s.error : undefined
|
||||
if (s?.status === "failed" || s?.status === "needs_client_registration") return s.error
|
||||
}
|
||||
const enabled = () => status() === "connected"
|
||||
return (
|
||||
@@ -76,9 +87,6 @@ export const DialogSelectMcp: Component = () => {
|
||||
<Show when={statusLabel()}>
|
||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||
</Show>
|
||||
<Show when={toggle.isPending && toggle.variables === i.name}>
|
||||
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={error()}>
|
||||
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { JSX } from "solid-js"
|
||||
|
||||
export type DialogGoUpsellProps = {
|
||||
title: string
|
||||
description: JSX.Element
|
||||
link?: string
|
||||
actionLabel: string
|
||||
onClose?: (dontShowAgain?: boolean) => void
|
||||
}
|
||||
|
||||
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
const dialog = useDialog()
|
||||
const platform = usePlatform()
|
||||
|
||||
const runAction = () => {
|
||||
if (props.link) platform.openLink(props.link)
|
||||
props.onClose?.()
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
const dismiss = () => {
|
||||
props.onClose?.(true)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={props.title} description={props.description} fit>
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={dismiss}>
|
||||
Don't show again
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={runAction}>
|
||||
{props.actionLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from "@/context/prompt"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -56,7 +55,8 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
@@ -99,11 +99,9 @@ const EXAMPLES = [
|
||||
"prompt.example.25",
|
||||
] as const
|
||||
|
||||
const NON_EMPTY_TEXT = /[^\s\u200B]/
|
||||
|
||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const sdk = useSDK()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const queryOptions = useQueryOptions()
|
||||
|
||||
const sync = useSync()
|
||||
const local = useLocal()
|
||||
@@ -240,13 +238,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return paths
|
||||
})
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const status = createMemo(
|
||||
() =>
|
||||
sync.data.session_status[params.id ?? ""] ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
const working = createMemo(() => status()?.type !== "idle")
|
||||
const working = createMemo(() => sync.data.session_working(params.id ?? ""))
|
||||
const imageAttachments = createMemo(() =>
|
||||
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
||||
)
|
||||
@@ -866,7 +858,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
? rawParts[0].content
|
||||
: rawParts.map((p) => ("content" in p ? p.content : "")).join("")
|
||||
const hasNonText = rawParts.some((part) => part.type !== "text")
|
||||
const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0
|
||||
const textContent = (editorRef.textContent ?? "").replace(/\u200B/g, "")
|
||||
const shouldReset =
|
||||
textContent.length === 0 && rawText.replace(/\n/g, "").length === 0 && !hasNonText && images.length === 0
|
||||
|
||||
if (shouldReset) {
|
||||
closePopover()
|
||||
@@ -1256,9 +1250,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||
queries: [
|
||||
loadAgentsQuery(sdk.directory, sdk.client),
|
||||
loadProvidersQuery(null, globalSDK.client),
|
||||
loadProvidersQuery(sdk.directory, sdk.client),
|
||||
queryOptions.agents(pathKey(sdk.directory)),
|
||||
queryOptions.providers(null),
|
||||
queryOptions.providers(pathKey(sdk.directory)),
|
||||
],
|
||||
}))
|
||||
|
||||
|
||||
@@ -123,11 +123,13 @@ function listFor(command: CommandContext, map: KeybindMap, palette: string) {
|
||||
|
||||
for (const opt of command.catalog) {
|
||||
if (opt.id.startsWith("suggested.")) continue
|
||||
if (opt.hidden) continue
|
||||
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
||||
}
|
||||
|
||||
for (const opt of command.options) {
|
||||
if (opt.id.startsWith("suggested.")) continue
|
||||
if (opt.hidden) continue
|
||||
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ import { useSDK } from "@/context/sdk"
|
||||
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
|
||||
import { mcpQueryKey } from "@/context/global-sync"
|
||||
import { useQueryOptions } from "@/context/global-sync"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const pollMs = 10_000
|
||||
|
||||
@@ -139,13 +140,22 @@ const useMcpToggleMutation = () => {
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
const queryClient = useQueryClient()
|
||||
const queryOptions = useQueryOptions()
|
||||
|
||||
return useMutation(() => ({
|
||||
mutationFn: async (name: string) => {
|
||||
const status = sync.data.mcp[name]
|
||||
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
return
|
||||
}
|
||||
if (status?.status === "needs_auth") {
|
||||
await sdk.client.mcp.auth.authenticate({ name })
|
||||
return
|
||||
}
|
||||
await sdk.client.mcp.connect({ name })
|
||||
},
|
||||
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
|
||||
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -323,7 +333,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean>; close?: ()
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||
class="flex items-center gap-2 w-full min-h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||
onClick={() => {
|
||||
if (toggleMcp.isPending) return
|
||||
toggleMcp.mutate(name)
|
||||
@@ -340,7 +350,16 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean>; close?: ()
|
||||
status() === "needs_auth" || status() === "needs_client_registration",
|
||||
}}
|
||||
/>
|
||||
<span class="text-14-regular text-text-base truncate flex-1">{name}</span>
|
||||
<span class="flex flex-col min-w-0 flex-1">
|
||||
<span class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-base truncate">{name}</span>
|
||||
</span>
|
||||
<Show when={status() === "needs_auth"}>
|
||||
<span class="text-11-regular text-text-weaker truncate">
|
||||
{language.t("mcp.auth.clickToAuthenticate")}
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
<div onClick={(event) => event.stopPropagation()}>
|
||||
<Switch
|
||||
checked={enabled()}
|
||||
|
||||
@@ -14,12 +14,14 @@ export function StatusPopover() {
|
||||
const sync = useSync()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
|
||||
const healthy = createMemo(() => {
|
||||
const serverHealthy = server.healthy() === true
|
||||
const mcpIssue = createMemo(() => {
|
||||
const mcp = Object.values(sync.data.mcp ?? {})
|
||||
const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled")
|
||||
return serverHealthy && !issue
|
||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
||||
if (failed) return "critical" as const
|
||||
if (warn) return "warning" as const
|
||||
})
|
||||
const healthy = createMemo(() => server.healthy() === true && !mcpIssue())
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@@ -41,7 +43,9 @@ export function StatusPopover() {
|
||||
classList={{
|
||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": ready() && healthy(),
|
||||
"bg-icon-critical-base": server.healthy() === false || (ready() && !healthy()),
|
||||
"bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning",
|
||||
"bg-icon-critical-base":
|
||||
server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"),
|
||||
"bg-border-weak-base": server.healthy() === undefined || !ready(),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface CommandOption {
|
||||
slash?: string
|
||||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
@@ -93,6 +94,7 @@ export type CommandCatalogItem = {
|
||||
category?: string
|
||||
keybind?: KeybindConfig
|
||||
slash?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type CommandRegistration = {
|
||||
@@ -279,13 +281,14 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
setCatalog(
|
||||
registered().reduce((acc, opt) => {
|
||||
const id = actionId(opt.id)
|
||||
acc[id] = {
|
||||
title: opt.title,
|
||||
description: opt.description,
|
||||
category: opt.category,
|
||||
keybind: opt.keybind,
|
||||
slash: opt.slash,
|
||||
}
|
||||
if (opt.title)
|
||||
acc[id] = {
|
||||
title: opt.title,
|
||||
description: opt.description,
|
||||
category: opt.category,
|
||||
keybind: opt.keybind,
|
||||
slash: opt.slash,
|
||||
}
|
||||
return acc
|
||||
}, {} as CommandCatalog),
|
||||
)
|
||||
|
||||
@@ -3,15 +3,13 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import z from "zod"
|
||||
import { createSdkForServer } from "@/utils/server"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
import { useServer } from "./server"
|
||||
|
||||
const abortError = z.object({
|
||||
name: z.literal("AbortError"),
|
||||
})
|
||||
const isAbortError = (error: unknown) =>
|
||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||
|
||||
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
||||
name: "GlobalSDK",
|
||||
@@ -103,7 +101,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
|
||||
let streamErrorLogged = false
|
||||
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
const aborted = (error: unknown) => abortError.safeParse(error).success
|
||||
const aborted = isAbortError
|
||||
|
||||
let attempt: AbortController | undefined
|
||||
let run: Promise<void> | undefined
|
||||
|
||||
@@ -18,8 +18,10 @@ import {
|
||||
bootstrapDirectory,
|
||||
bootstrapGlobal,
|
||||
clearProviderRev,
|
||||
loadAgentsQuery,
|
||||
loadGlobalConfigQuery,
|
||||
loadPathQuery,
|
||||
loadProjectsQuery,
|
||||
loadProvidersQuery,
|
||||
} from "./global-sync/bootstrap"
|
||||
import { createChildStoreManager } from "./global-sync/child-store"
|
||||
@@ -33,6 +35,7 @@ import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
|
||||
import { createRefreshQueue } from "./global-sync/queue"
|
||||
import { directoryKey } from "./global-sync/utils"
|
||||
import { PathKey } from "@/utils/path-key"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -48,24 +51,33 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
|
||||
|
||||
export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
|
||||
|
||||
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: mcpQueryKey(directory),
|
||||
queryKey: [directory, "mcp"] as const,
|
||||
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
|
||||
})
|
||||
|
||||
export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
|
||||
|
||||
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
|
||||
queryOptions({
|
||||
queryKey: lspQueryKey(directory),
|
||||
queryKey: [directory, "lsp"] as const,
|
||||
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
|
||||
})
|
||||
|
||||
function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
|
||||
return {
|
||||
globalConfig: () => loadGlobalConfigQuery(globalSDK()),
|
||||
projects: () => loadProjectsQuery(globalSDK()),
|
||||
providers: (directory: PathKey | null) =>
|
||||
loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
|
||||
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
|
||||
sessions: (directory: PathKey) => ({ queryKey: [directory, "loadSessions"] as const }),
|
||||
}
|
||||
}
|
||||
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
@@ -77,12 +89,22 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const sdkFor = (directory: string) => {
|
||||
const key = directoryKey(directory)
|
||||
const cached = sdkCache.get(key)
|
||||
if (cached) return cached
|
||||
const sdk = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
sdkCache.set(key, sdk)
|
||||
return sdk
|
||||
}
|
||||
|
||||
const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor)
|
||||
|
||||
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
|
||||
queries: [
|
||||
loadGlobalConfigQuery(globalSDK.client),
|
||||
loadProvidersQuery(null, globalSDK.client),
|
||||
loadPathQuery(null, globalSDK.client),
|
||||
],
|
||||
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
|
||||
}))
|
||||
|
||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||
@@ -181,18 +203,6 @@ function createGlobalSync() {
|
||||
bootstrapInstance,
|
||||
})
|
||||
|
||||
const sdkFor = (directory: string) => {
|
||||
const key = directoryKey(directory)
|
||||
const cached = sdkCache.get(key)
|
||||
if (cached) return cached
|
||||
const sdk = globalSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
})
|
||||
sdkCache.set(key, sdk)
|
||||
return sdk
|
||||
}
|
||||
|
||||
const children = createChildStoreManager({
|
||||
owner,
|
||||
isBooting: (directory) => booting.has(directory),
|
||||
@@ -209,7 +219,7 @@ function createGlobalSync() {
|
||||
clearSessionPrefetchDirectory(key)
|
||||
},
|
||||
translate: language.t,
|
||||
getSdk: sdkFor,
|
||||
queryOptions: queryOptionsApi,
|
||||
global: {
|
||||
provider: globalStore.provider,
|
||||
},
|
||||
@@ -239,7 +249,7 @@ function createGlobalSync() {
|
||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||
const promise = queryClient
|
||||
.fetchQuery({
|
||||
queryKey: loadSessionsQueryKey(key),
|
||||
...queryOptionsApi.sessions(key),
|
||||
queryFn: () =>
|
||||
loadRootSessionsWithFallback({
|
||||
directory,
|
||||
@@ -368,7 +378,7 @@ function createGlobalSync() {
|
||||
setSessionTodo,
|
||||
vcsCache: children.vcsCache.get(key),
|
||||
loadLsp: () => {
|
||||
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
|
||||
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -422,6 +432,7 @@ function createGlobalSync() {
|
||||
},
|
||||
child: children.child,
|
||||
peek: children.peek,
|
||||
queryOptions: queryOptionsApi,
|
||||
// bootstrap,
|
||||
updateConfig: updateConfigMutation.mutateAsync,
|
||||
project: projectApi,
|
||||
@@ -443,3 +454,7 @@ export function useGlobalSync() {
|
||||
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
|
||||
return context
|
||||
}
|
||||
|
||||
export function useQueryOptions() {
|
||||
return useGlobalSync().queryOptions
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("createChildStoreManager", () => {
|
||||
onBootstrap() {},
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
getSdk: () => null!,
|
||||
queryOptions: {} as any,
|
||||
global: { provider: null! },
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
DIR_IDLE_TTL_MS,
|
||||
MAX_DIR_STORES,
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
|
||||
import { loadLspQuery, loadMcpQuery } from "../global-sync"
|
||||
import { QueryOptionsApi } from "../global-sync"
|
||||
import { directoryKey, type DirectoryKey } from "./utils"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
@@ -26,7 +25,7 @@ export function createChildStoreManager(input: {
|
||||
onBootstrap: (directory: string) => void
|
||||
onDispose: (directory: string) => void
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
getSdk: (directory: string) => OpencodeClient
|
||||
queryOptions: QueryOptionsApi
|
||||
global: {
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
@@ -182,17 +181,15 @@ export function createChildStoreManager(input: {
|
||||
|
||||
const init = () =>
|
||||
createRoot((dispose) => {
|
||||
const sdk = input.getSdk(directory)
|
||||
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
|
||||
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
|
||||
queries: [
|
||||
loadPathQuery(key, sdk),
|
||||
loadMcpQuery(key, sdk),
|
||||
loadLspQuery(key, sdk),
|
||||
loadProvidersQuery(key, sdk),
|
||||
input.queryOptions.path(key),
|
||||
input.queryOptions.mcp(key),
|
||||
input.queryOptions.lsp(key),
|
||||
input.queryOptions.providers(key),
|
||||
],
|
||||
}))
|
||||
|
||||
@@ -222,6 +219,10 @@ export function createChildStoreManager(input: {
|
||||
session: [],
|
||||
sessionTotal: 0,
|
||||
session_status: {},
|
||||
session_working(id: string) {
|
||||
const type = this.session_status[id]?.type
|
||||
return (type ?? "idle") !== "idle"
|
||||
},
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
@@ -242,6 +243,7 @@ export function createChildStoreManager(input: {
|
||||
limit: 5,
|
||||
message: {},
|
||||
part: {},
|
||||
part_text_accum_delta: {},
|
||||
})
|
||||
children[key] = child
|
||||
disposers.set(key, dispose)
|
||||
|
||||
@@ -81,6 +81,7 @@ const baseState = (input: Partial<State> = {}) =>
|
||||
limit: 10,
|
||||
message: {},
|
||||
part: {},
|
||||
part_text_accum_delta: {},
|
||||
...input,
|
||||
}) as State
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ export function applyDirectoryEvent(input: {
|
||||
const info = (event.properties as { info: Session }).info
|
||||
const result = Binary.search(input.store.session, info.id, (s) => s.id)
|
||||
if (info.time.archived) {
|
||||
if (input.store.session[result.index]!.time.archived === info.time.archived) break
|
||||
if (result.found) {
|
||||
input.setStore(
|
||||
"session",
|
||||
@@ -211,6 +212,12 @@ export function applyDirectoryEvent(input: {
|
||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
const parts = draft.part[props.messageID]
|
||||
if (parts) {
|
||||
for (const part of parts) {
|
||||
delete draft.part_text_accum_delta[part.id]
|
||||
}
|
||||
}
|
||||
delete draft.part[props.messageID]
|
||||
}),
|
||||
)
|
||||
@@ -219,6 +226,11 @@ export function applyDirectoryEvent(input: {
|
||||
case "message.part.updated": {
|
||||
const part = (event.properties as { part: Part }).part
|
||||
if (SKIP_PARTS.has(part.type)) break
|
||||
input.setStore(
|
||||
produce((draft) => {
|
||||
delete draft.part_text_accum_delta[part.id]
|
||||
}),
|
||||
)
|
||||
const parts = input.store.part[part.messageID]
|
||||
if (!parts) {
|
||||
input.setStore("part", part.messageID, [part])
|
||||
@@ -240,6 +252,11 @@ export function applyDirectoryEvent(input: {
|
||||
}
|
||||
case "message.part.removed": {
|
||||
const props = event.properties as { messageID: string; partID: string }
|
||||
input.setStore(
|
||||
produce((draft) => {
|
||||
delete draft.part_text_accum_delta[props.partID]
|
||||
}),
|
||||
)
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
@@ -263,6 +280,7 @@ export function applyDirectoryEvent(input: {
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
if (!result.found) break
|
||||
input.setStore("part_text_accum_delta", props.partID, (existing) => (existing ?? "") + props.delta)
|
||||
input.setStore(
|
||||
"part",
|
||||
props.messageID,
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("app session cache", () => {
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
session_status: { ses_1: { type: "busy" } as SessionStatus },
|
||||
session_diff: { ses_1: [] },
|
||||
@@ -47,12 +48,14 @@ describe("app session cache", () => {
|
||||
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
||||
permission: { ses_1: [] as PermissionRequest[] },
|
||||
question: { ses_1: [] as QuestionRequest[] },
|
||||
part_text_accum_delta: { prt_1: "streamed text" },
|
||||
}
|
||||
|
||||
dropSessionCaches(store, ["ses_1"])
|
||||
|
||||
expect(store.message.ses_1).toBeUndefined()
|
||||
expect(store.part.msg_1).toBeUndefined()
|
||||
expect(store.part_text_accum_delta.prt_1).toBeUndefined()
|
||||
expect(store.todo.ses_1).toBeUndefined()
|
||||
expect(store.session_diff.ses_1).toBeUndefined()
|
||||
expect(store.session_status.ses_1).toBeUndefined()
|
||||
@@ -70,6 +73,7 @@ describe("app session cache", () => {
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
@@ -78,6 +82,7 @@ describe("app session cache", () => {
|
||||
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
|
||||
permission: {},
|
||||
question: {},
|
||||
part_text_accum_delta: {},
|
||||
}
|
||||
|
||||
dropSessionCaches(store, ["ses_1"])
|
||||
|
||||
@@ -18,6 +18,7 @@ type SessionCache = {
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
|
||||
@@ -27,6 +28,9 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
|
||||
for (const key of Object.keys(store.part)) {
|
||||
const parts = store.part[key]
|
||||
if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
|
||||
for (const part of parts) {
|
||||
delete store.part_text_accum_delta[part.id]
|
||||
}
|
||||
delete store.part[key]
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export function trimSessions(
|
||||
.filter((s) => !s.time?.archived)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
const roots = all.filter((s) => !s.parentID)
|
||||
roots.sort(compareSessionRecent)
|
||||
const children = all.filter((s) => !!s.parentID)
|
||||
const base = roots.slice(0, limit)
|
||||
const recent = takeRecentSessions(roots.slice(limit), SESSION_RECENT_LIMIT, cutoff)
|
||||
|
||||
@@ -46,6 +46,7 @@ export type State = {
|
||||
session_status: {
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
session_working(id: string): boolean
|
||||
session_diff: {
|
||||
[sessionID: string]: SnapshotFileDiff[]
|
||||
}
|
||||
@@ -72,6 +73,9 @@ export type State = {
|
||||
part: {
|
||||
[messageID: string]: Part[]
|
||||
}
|
||||
part_text_accum_delta: {
|
||||
[partID: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type VcsCache = {
|
||||
|
||||
@@ -43,6 +43,7 @@ type SessionView = {
|
||||
reviewOpen?: string[]
|
||||
pendingMessage?: string
|
||||
pendingMessageAt?: number
|
||||
todoCollapsed?: boolean
|
||||
}
|
||||
|
||||
type TabHandoff = {
|
||||
@@ -759,6 +760,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setScroll(tab: string, pos: SessionScroll) {
|
||||
scroll.setScroll(key(), tab, pos)
|
||||
},
|
||||
todoCollapsed: {
|
||||
get: () => s().todoCollapsed ?? false,
|
||||
set(collapsed: boolean) {
|
||||
const session = key()
|
||||
const current = store.sessionView[session]
|
||||
if (!current) {
|
||||
setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
|
||||
} else {
|
||||
setStore("sessionView", session, "todoCollapsed", collapsed)
|
||||
}
|
||||
},
|
||||
},
|
||||
terminal: {
|
||||
opened: terminalOpened,
|
||||
open() {
|
||||
|
||||
@@ -44,7 +44,7 @@ const migrate = (value: unknown) => {
|
||||
}
|
||||
|
||||
const clone = (value: State | undefined) => {
|
||||
if (!value) return undefined
|
||||
if (!value) return
|
||||
return {
|
||||
...value,
|
||||
model: value.model ? { ...value.model } : undefined,
|
||||
@@ -104,7 +104,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const pickAgent = (name: string | undefined) => {
|
||||
const items = list()
|
||||
if (items.length === 0) return undefined
|
||||
if (items.length === 0) return
|
||||
return items.find((item) => item.name === name) ?? items[0]
|
||||
}
|
||||
|
||||
@@ -227,14 +227,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
() => agent.current()?.model,
|
||||
fallback,
|
||||
)
|
||||
if (!item) return undefined
|
||||
if (!item) return
|
||||
return models.find(item)
|
||||
}
|
||||
|
||||
const configured = () => {
|
||||
const item = agent.current()
|
||||
const model = current()
|
||||
if (!item || !model) return undefined
|
||||
if (!item || !model) return
|
||||
return getConfiguredAgentVariant({
|
||||
agent: { model: item.model, variant: item.variant },
|
||||
model: { providerID: model.provider.id, modelID: model.id, variants: model.variants },
|
||||
@@ -314,11 +314,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
configured,
|
||||
selected,
|
||||
current() {
|
||||
return resolveModelVariant({
|
||||
const resolved = resolveModelVariant({
|
||||
variants: this.list(),
|
||||
selected: this.selected(),
|
||||
configured: this.configured(),
|
||||
})
|
||||
if (resolved) return resolved
|
||||
const model = current()
|
||||
if (!model) return
|
||||
const saved = models.variant.get({ providerID: model.provider.id, modelID: model.id })
|
||||
if (saved && this.list().includes(saved)) return saved
|
||||
},
|
||||
list() {
|
||||
const item = current()
|
||||
@@ -335,6 +340,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
variant: value ?? null,
|
||||
})
|
||||
write({ variant: value ?? null })
|
||||
if (model) {
|
||||
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value ?? undefined)
|
||||
}
|
||||
})
|
||||
},
|
||||
cycle() {
|
||||
|
||||
@@ -276,6 +276,7 @@ export const dict = {
|
||||
"mcp.status.connected": "متصل",
|
||||
"mcp.status.failed": "فشل",
|
||||
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
|
||||
"mcp.auth.clickToAuthenticate": "انقر للمصادقة",
|
||||
"mcp.status.disabled": "معطل",
|
||||
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
|
||||
"dialog.directory.search.placeholder": "البحث في المجلدات",
|
||||
|
||||
@@ -276,6 +276,7 @@ export const dict = {
|
||||
"mcp.status.connected": "conectado",
|
||||
"mcp.status.failed": "falhou",
|
||||
"mcp.status.needs_auth": "precisa de autenticação",
|
||||
"mcp.auth.clickToAuthenticate": "Clique para autenticar",
|
||||
"mcp.status.disabled": "desabilitado",
|
||||
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
|
||||
"dialog.directory.search.placeholder": "Buscar pastas",
|
||||
|
||||
@@ -300,6 +300,7 @@ export const dict = {
|
||||
"mcp.status.connected": "povezano",
|
||||
"mcp.status.failed": "neuspjelo",
|
||||
"mcp.status.needs_auth": "potrebna autentifikacija",
|
||||
"mcp.auth.clickToAuthenticate": "Kliknite za autentifikaciju",
|
||||
"mcp.status.disabled": "onemogućeno",
|
||||
|
||||
"dialog.fork.empty": "Nema poruka za fork",
|
||||
|
||||
@@ -298,6 +298,7 @@ export const dict = {
|
||||
"mcp.status.connected": "forbundet",
|
||||
"mcp.status.failed": "mislykkedes",
|
||||
"mcp.status.needs_auth": "kræver godkendelse",
|
||||
"mcp.auth.clickToAuthenticate": "Klik for at godkende",
|
||||
"mcp.status.disabled": "deaktiveret",
|
||||
|
||||
"dialog.fork.empty": "Ingen beskeder at forgrene fra",
|
||||
|
||||
@@ -282,6 +282,7 @@ export const dict = {
|
||||
"mcp.status.connected": "verbunden",
|
||||
"mcp.status.failed": "fehlgeschlagen",
|
||||
"mcp.status.needs_auth": "benötigt Authentifizierung",
|
||||
"mcp.auth.clickToAuthenticate": "Zum Authentifizieren klicken",
|
||||
"mcp.status.disabled": "deaktiviert",
|
||||
"dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
|
||||
"dialog.directory.search.placeholder": "Ordner durchsuchen",
|
||||
|
||||
@@ -25,6 +25,7 @@ export const dict = {
|
||||
"command.project.open": "Open project",
|
||||
"command.project.previous": "Previous project",
|
||||
"command.project.next": "Next project",
|
||||
"command.project.index": "Switch to project {{index}}",
|
||||
"command.provider.connect": "Connect provider",
|
||||
"command.server.switch": "Switch server",
|
||||
"command.settings.open": "Open settings",
|
||||
@@ -305,6 +306,7 @@ export const dict = {
|
||||
"mcp.status.failed": "failed",
|
||||
"mcp.status.needs_auth": "needs auth",
|
||||
"mcp.status.disabled": "disabled",
|
||||
"mcp.auth.clickToAuthenticate": "Click to authenticate",
|
||||
|
||||
"dialog.fork.empty": "No messages to fork from",
|
||||
|
||||
@@ -901,7 +903,7 @@ export const dict = {
|
||||
"settings.permissions.tool.read.title": "Read",
|
||||
"settings.permissions.tool.read.description": "Reading a file (matches the file path)",
|
||||
"settings.permissions.tool.edit.title": "Edit",
|
||||
"settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits",
|
||||
"settings.permissions.tool.edit.description": "Modify files, including edits, writes, and patches",
|
||||
"settings.permissions.tool.glob.title": "Glob",
|
||||
"settings.permissions.tool.glob.description": "Match files using glob patterns",
|
||||
"settings.permissions.tool.grep.title": "Grep",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const dict = {
|
||||
"mcp.status.connected": "conectado",
|
||||
"mcp.status.failed": "fallido",
|
||||
"mcp.status.needs_auth": "necesita auth",
|
||||
"mcp.auth.clickToAuthenticate": "Haz clic para autenticar",
|
||||
"mcp.status.disabled": "deshabilitado",
|
||||
|
||||
"dialog.fork.empty": "No hay mensajes desde donde bifurcar",
|
||||
|
||||
@@ -277,6 +277,7 @@ export const dict = {
|
||||
"mcp.status.connected": "connecté",
|
||||
"mcp.status.failed": "échoué",
|
||||
"mcp.status.needs_auth": "nécessite auth",
|
||||
"mcp.auth.clickToAuthenticate": "Cliquez pour vous authentifier",
|
||||
"mcp.status.disabled": "désactivé",
|
||||
"dialog.fork.empty": "Aucun message à partir duquel bifurquer",
|
||||
"dialog.directory.search.placeholder": "Rechercher des dossiers",
|
||||
|
||||
@@ -275,6 +275,7 @@ export const dict = {
|
||||
"mcp.status.connected": "接続済み",
|
||||
"mcp.status.failed": "失敗",
|
||||
"mcp.status.needs_auth": "認証が必要",
|
||||
"mcp.auth.clickToAuthenticate": "クリックして認証",
|
||||
"mcp.status.disabled": "無効",
|
||||
"dialog.fork.empty": "フォーク元のメッセージがありません",
|
||||
"dialog.directory.search.placeholder": "フォルダを検索",
|
||||
|
||||
@@ -275,6 +275,7 @@ export const dict = {
|
||||
"mcp.status.connected": "연결됨",
|
||||
"mcp.status.failed": "실패",
|
||||
"mcp.status.needs_auth": "인증 필요",
|
||||
"mcp.auth.clickToAuthenticate": "클릭하여 인증",
|
||||
"mcp.status.disabled": "비활성화됨",
|
||||
"dialog.fork.empty": "분기할 메시지 없음",
|
||||
"dialog.directory.search.placeholder": "폴더 검색",
|
||||
|
||||
@@ -302,6 +302,7 @@ export const dict = {
|
||||
"mcp.status.connected": "tilkoblet",
|
||||
"mcp.status.failed": "mislyktes",
|
||||
"mcp.status.needs_auth": "trenger autentisering",
|
||||
"mcp.auth.clickToAuthenticate": "Klikk for å autentisere",
|
||||
"mcp.status.disabled": "deaktivert",
|
||||
|
||||
"dialog.fork.empty": "Ingen meldinger å forgrene fra",
|
||||
|
||||
@@ -277,6 +277,7 @@ export const dict = {
|
||||
"mcp.status.connected": "połączono",
|
||||
"mcp.status.failed": "niepowodzenie",
|
||||
"mcp.status.needs_auth": "wymaga autoryzacji",
|
||||
"mcp.auth.clickToAuthenticate": "Kliknij, aby się uwierzytelnić",
|
||||
"mcp.status.disabled": "wyłączone",
|
||||
"dialog.fork.empty": "Brak wiadomości do rozwidlenia",
|
||||
"dialog.directory.search.placeholder": "Szukaj folderów",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const dict = {
|
||||
"mcp.status.connected": "подключено",
|
||||
"mcp.status.failed": "ошибка",
|
||||
"mcp.status.needs_auth": "требуется авторизация",
|
||||
"mcp.auth.clickToAuthenticate": "Нажмите, чтобы авторизоваться",
|
||||
"mcp.status.disabled": "отключено",
|
||||
|
||||
"dialog.fork.empty": "Нет сообщений для ответвления",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const dict = {
|
||||
"mcp.status.connected": "เชื่อมต่อแล้ว",
|
||||
"mcp.status.failed": "ล้มเหลว",
|
||||
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
|
||||
"mcp.auth.clickToAuthenticate": "คลิกเพื่อยืนยันตัวตน",
|
||||
"mcp.status.disabled": "ปิดใช้งาน",
|
||||
|
||||
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",
|
||||
|
||||
@@ -304,6 +304,7 @@ export const dict = {
|
||||
"mcp.status.connected": "bağlı",
|
||||
"mcp.status.failed": "başarısız",
|
||||
"mcp.status.needs_auth": "kimlik doğrulama gerekli",
|
||||
"mcp.auth.clickToAuthenticate": "Kimlik doğrulamak için tıklayın",
|
||||
"mcp.status.disabled": "devre dışı",
|
||||
|
||||
"dialog.fork.empty": "Dallandırılacak mesaj yok",
|
||||
|
||||
@@ -319,6 +319,7 @@ export const dict = {
|
||||
"mcp.status.connected": "已连接",
|
||||
"mcp.status.failed": "失败",
|
||||
"mcp.status.needs_auth": "需要授权",
|
||||
"mcp.auth.clickToAuthenticate": "点击进行授权",
|
||||
"mcp.status.disabled": "已禁用",
|
||||
|
||||
"dialog.fork.empty": "没有可用于分叉的消息",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const dict = {
|
||||
"mcp.status.connected": "已連線",
|
||||
"mcp.status.failed": "失敗",
|
||||
"mcp.status.needs_auth": "需要授權",
|
||||
"mcp.auth.clickToAuthenticate": "點擊以進行授權",
|
||||
"mcp.status.disabled": "已停用",
|
||||
|
||||
"dialog.fork.empty": "沒有可用於分支的訊息",
|
||||
|
||||
@@ -961,6 +961,15 @@ export default function Layout(props: ParentProps) {
|
||||
void openProject(target.worktree)
|
||||
}
|
||||
|
||||
function navigateToProjectIndex(index: number) {
|
||||
const projects = layout.projects.list()
|
||||
const target = projects[index]
|
||||
if (!target) return
|
||||
|
||||
globalSync.child(target.worktree)
|
||||
void openProject(target.worktree)
|
||||
}
|
||||
|
||||
function navigateSessionByUnseen(offset: number) {
|
||||
const sessions = currentSessions()
|
||||
if (sessions.length === 0) return
|
||||
@@ -1041,6 +1050,19 @@ export default function Layout(props: ParentProps) {
|
||||
keybind: "mod+alt+arrowdown",
|
||||
onSelect: () => navigateProjectByOffset(1),
|
||||
},
|
||||
...Array.from({ length: 9 }, (_, i) => {
|
||||
const index = i
|
||||
const number = index + 1
|
||||
return {
|
||||
id: `project.${number}`,
|
||||
category: language.t("command.category.project"),
|
||||
title: `Open Project {number}`,
|
||||
keybind: `mod+${number}`,
|
||||
disabled: layout.projects.list().length <= index,
|
||||
hidden: true,
|
||||
onSelect: () => navigateToProjectIndex(index),
|
||||
}
|
||||
}),
|
||||
{
|
||||
id: "provider.connect",
|
||||
title: language.t("command.provider.connect"),
|
||||
@@ -1410,19 +1432,20 @@ export default function Layout(props: ParentProps) {
|
||||
const index = list.findIndex((x) => pathKey(x.worktree) === key)
|
||||
const active = pathKey(currentProject()?.worktree ?? "") === key
|
||||
if (index === -1) return
|
||||
const next = list[index + 1]
|
||||
|
||||
if (!active) {
|
||||
layout.projects.close(directory)
|
||||
return
|
||||
}
|
||||
|
||||
if (!next) {
|
||||
if (list.length === 1) {
|
||||
layout.projects.close(directory)
|
||||
navigate("/")
|
||||
return
|
||||
}
|
||||
|
||||
const next = list[index + 1] ?? list[index - 1]
|
||||
|
||||
navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`)
|
||||
layout.projects.close(directory)
|
||||
queueMicrotask(() => {
|
||||
@@ -1935,7 +1958,7 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
if (!created?.directory) return
|
||||
|
||||
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
|
||||
setWorkspaceName(created.directory, created.branch ?? getFilename(created.directory), project.id, created.branch)
|
||||
|
||||
const local = project.worktree
|
||||
const key = pathKey(created.directory)
|
||||
@@ -2097,6 +2120,7 @@ export default function Layout(props: ParentProps) {
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
keyed
|
||||
>
|
||||
{(project) => (
|
||||
<>
|
||||
@@ -2107,9 +2131,7 @@ export default function Layout(props: ParentProps) {
|
||||
id={`project:${projectId()}`}
|
||||
value={projectName}
|
||||
onSave={(next) => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void renameProject(item, next)
|
||||
void renameProject(project, next)
|
||||
}}
|
||||
class="text-14-medium text-text-strong truncate"
|
||||
displayClass="text-14-medium text-text-strong truncate"
|
||||
@@ -2151,9 +2173,7 @@ export default function Layout(props: ParentProps) {
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
showEditProjectDialog(item)
|
||||
showEditProjectDialog(project)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||
@@ -2163,9 +2183,7 @@ export default function Layout(props: ParentProps) {
|
||||
data-project={slug()}
|
||||
disabled={!canToggle()}
|
||||
onSelect={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
toggleProjectWorkspaces(item)
|
||||
toggleProjectWorkspaces(project)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
@@ -2224,7 +2242,7 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
project={project()}
|
||||
project={project}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
@@ -2239,9 +2257,7 @@ export default function Layout(props: ParentProps) {
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
const item = project()
|
||||
if (!item) return
|
||||
void createWorkspace(item)
|
||||
void createWorkspace(project)
|
||||
}}
|
||||
>
|
||||
{language.t("workspace.new")}
|
||||
@@ -2268,7 +2284,7 @@ export default function Layout(props: ParentProps) {
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={project()}
|
||||
project={project}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,12 @@ export function getProjectAvatarSource(id?: string, icon?: { color?: string; url
|
||||
return icon?.url
|
||||
}
|
||||
|
||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||
export const ProjectIcon = (props: {
|
||||
project: LocalProject
|
||||
class?: string
|
||||
notify?: boolean
|
||||
working?: boolean
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const notification = useNotification()
|
||||
const permission = usePermission()
|
||||
@@ -65,6 +70,11 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={props.working}>
|
||||
<div class="absolute bottom-px right-px size-3 rounded-full bg-background-base z-10 flex items-center justify-center">
|
||||
<Spinner class="size-[9px]" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -156,18 +166,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
})
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
||||
(message) =>
|
||||
message.role === "assistant" &&
|
||||
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
|
||||
)
|
||||
const status = sessionStore.session_status[props.session.id]
|
||||
return (
|
||||
pending !== undefined ||
|
||||
status?.type === "busy" ||
|
||||
status?.type === "retry" ||
|
||||
(status !== undefined && status.type !== "idle")
|
||||
)
|
||||
return sessionStore.session_working(props.session.id)
|
||||
})
|
||||
|
||||
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
|
||||
|
||||
@@ -56,6 +56,7 @@ const ProjectTile = (props: {
|
||||
sidebarHovering: Accessor<boolean>
|
||||
selected: Accessor<boolean>
|
||||
active: Accessor<boolean>
|
||||
isWorking: Accessor<boolean>
|
||||
overlay: Accessor<boolean>
|
||||
suppressHover: Accessor<boolean>
|
||||
dirs: Accessor<string[]>
|
||||
@@ -143,7 +144,7 @@ const ProjectTile = (props: {
|
||||
}}
|
||||
onBlur={() => props.setOpen(false)}
|
||||
>
|
||||
<ProjectIcon project={props.project} notify />
|
||||
<ProjectIcon project={props.project} notify working={props.isWorking()} />
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content>
|
||||
@@ -301,6 +302,12 @@ export const SortableProject = (props: {
|
||||
}
|
||||
|
||||
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||
const isWorking = createMemo(() =>
|
||||
dirs().some((directory) => {
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
return Object.keys(store.session_status).some((id) => store.session_working(id))
|
||||
}),
|
||||
)
|
||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
||||
const workspaceSessions = (directory: string) => {
|
||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||
@@ -313,6 +320,7 @@ export const SortableProject = (props: {
|
||||
sidebarHovering={props.ctx.sidebarHovering}
|
||||
selected={selected}
|
||||
active={active}
|
||||
isWorking={isWorking}
|
||||
overlay={overlay}
|
||||
suppressHover={() => state.suppressHover}
|
||||
dirs={dirs}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
@@ -300,6 +300,7 @@ export const SortableWorkspace = (props: {
|
||||
const navigate = useNavigate()
|
||||
const params = useParams()
|
||||
const globalSync = useGlobalSync()
|
||||
const queryOptions = useQueryOptions()
|
||||
const language = useLanguage()
|
||||
const sortable = createSortable(props.directory)
|
||||
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
||||
@@ -320,7 +321,7 @@ export const SortableWorkspace = (props: {
|
||||
const boot = createMemo(() => open() || active())
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) }))
|
||||
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.directory)))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const loading = () => fetching() > 0 && count() === 0
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
@@ -446,6 +447,7 @@ export const LocalWorkspace = (props: {
|
||||
mobile?: boolean
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const queryOptions = useQueryOptions()
|
||||
const language = useLanguage()
|
||||
const workspace = createMemo(() => {
|
||||
const [store, setStore] = globalSync.child(props.project.worktree)
|
||||
@@ -454,7 +456,7 @@ export const LocalWorkspace = (props: {
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) }))
|
||||
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.project.worktree)))
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||
const loading = () => fetching() > 0 && count() === 0
|
||||
const loadMore = async () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { useSearchParams } from "@solidjs/router"
|
||||
import { useLocation, useSearchParams } from "@solidjs/router"
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||
@@ -64,6 +64,7 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { same } from "@/utils/same"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
type FollowupItem = FollowupDraft & { id: string }
|
||||
@@ -75,7 +76,6 @@ type VcsMode = "git" | "branch"
|
||||
|
||||
type SessionHistoryWindowInput = {
|
||||
sessionID: () => string | undefined
|
||||
messagesReady: () => boolean
|
||||
loaded: () => number
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
historyMore: () => boolean
|
||||
@@ -85,205 +85,74 @@ type SessionHistoryWindowInput = {
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the rendered history window for a session timeline.
|
||||
*
|
||||
* It keeps initial paint bounded to recent turns, reveals cached turns in
|
||||
* small batches while scrolling upward, and prefetches older history near top.
|
||||
*/
|
||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
const turnInit = 10
|
||||
const turnBatch = 8
|
||||
const turnScrollThreshold = 200
|
||||
const turnPrefetchBuffer = 16
|
||||
const prefetchCooldownMs = 400
|
||||
const prefetchNoGrowthLimit = 2
|
||||
function createSessionHistoryLoader(input: SessionHistoryWindowInput) {
|
||||
const historyScrollThreshold = 200
|
||||
let shiftFrame: number | undefined
|
||||
|
||||
const [state, setState] = createStore({
|
||||
turnID: undefined as string | undefined,
|
||||
turnStart: 0,
|
||||
prefetchUntil: 0,
|
||||
prefetchNoGrowth: 0,
|
||||
shift: false,
|
||||
})
|
||||
|
||||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
||||
|
||||
const turnStart = createMemo(() => {
|
||||
const id = input.sessionID()
|
||||
const len = input.visibleUserMessages().length
|
||||
if (!id || len <= 0) return 0
|
||||
if (state.turnID !== id) return initialTurnStart(len)
|
||||
if (state.turnStart <= 0) return 0
|
||||
if (state.turnStart >= len) return initialTurnStart(len)
|
||||
return state.turnStart
|
||||
const userMessages = createMemo(() => input.visibleUserMessages(), emptyUserMessages, {
|
||||
equals: same,
|
||||
})
|
||||
|
||||
const setTurnStart = (start: number) => {
|
||||
const id = input.sessionID()
|
||||
const next = start > 0 ? start : 0
|
||||
if (!id) {
|
||||
setState({ turnID: undefined, turnStart: next })
|
||||
return
|
||||
}
|
||||
setState({ turnID: id, turnStart: next })
|
||||
const cancelShiftReset = () => {
|
||||
if (shiftFrame === undefined) return
|
||||
cancelAnimationFrame(shiftFrame)
|
||||
shiftFrame = undefined
|
||||
}
|
||||
|
||||
const renderedUserMessages = createMemo(
|
||||
() => {
|
||||
const msgs = input.visibleUserMessages()
|
||||
const start = turnStart()
|
||||
if (start <= 0) return msgs
|
||||
return msgs.slice(start)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{
|
||||
equals: same,
|
||||
},
|
||||
)
|
||||
|
||||
const preserveScroll = (fn: () => void) => {
|
||||
const el = input.scroller()
|
||||
if (!el) {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
fn()
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
const scheduleShiftReset = () => {
|
||||
cancelShiftReset()
|
||||
shiftFrame = requestAnimationFrame(() => {
|
||||
shiftFrame = undefined
|
||||
setState("shift", false)
|
||||
})
|
||||
}
|
||||
|
||||
const backfillTurns = () => {
|
||||
const start = turnStart()
|
||||
if (start <= 0) return
|
||||
|
||||
const next = start - turnBatch
|
||||
const nextStart = next > 0 ? next : 0
|
||||
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||
const loadAndReveal = async () => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
|
||||
const start = turnStart()
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
let loaded = input.loaded()
|
||||
|
||||
if (start > 0) setTurnStart(0)
|
||||
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
let afterVisible = beforeVisible
|
||||
let added = 0
|
||||
|
||||
while (true) {
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
afterVisible = input.visibleUserMessages().length
|
||||
const nextLoaded = input.loaded()
|
||||
const raw = nextLoaded - loaded
|
||||
added += raw
|
||||
loaded = nextLoaded
|
||||
|
||||
if (afterVisible > beforeVisible) break
|
||||
if (raw <= 0) break
|
||||
if (!input.historyMore()) break
|
||||
}
|
||||
|
||||
if (added <= 0) return
|
||||
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
||||
|
||||
const growth = afterVisible - beforeVisible
|
||||
if (growth <= 0) return
|
||||
if (turnStart() !== 0) return
|
||||
|
||||
const target = Math.min(afterVisible, beforeVisible + turnBatch)
|
||||
setTurnStart(Math.max(0, afterVisible - target))
|
||||
}
|
||||
|
||||
/** Scroll/prefetch path: fetch older history from server. */
|
||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
||||
const fetchOlderMessages = async () => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
if (opts?.prefetch) {
|
||||
const now = Date.now()
|
||||
if (state.prefetchUntil > now) return
|
||||
if (state.prefetchNoGrowth >= prefetchNoGrowthLimit) return
|
||||
setState("prefetchUntil", now + prefetchCooldownMs)
|
||||
}
|
||||
|
||||
const start = turnStart()
|
||||
// TODO(session-timeline): switch this to core cursor-based part pagination when that API lands.
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
||||
let loaded = input.loaded()
|
||||
let added = 0
|
||||
let growth = 0
|
||||
|
||||
cancelShiftReset()
|
||||
setState("shift", true)
|
||||
|
||||
while (true) {
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
const nextLoaded = input.loaded()
|
||||
const raw = nextLoaded - loaded
|
||||
added += raw
|
||||
loaded = nextLoaded
|
||||
growth = input.visibleUserMessages().length - beforeVisible
|
||||
|
||||
if (growth > 0) break
|
||||
if (raw <= 0) break
|
||||
if (opts?.prefetch) break
|
||||
if (!input.historyMore()) break
|
||||
}
|
||||
|
||||
const afterVisible = input.visibleUserMessages().length
|
||||
|
||||
if (opts?.prefetch) {
|
||||
setState("prefetchNoGrowth", added > 0 ? 0 : state.prefetchNoGrowth + 1)
|
||||
} else if (added > 0 && state.prefetchNoGrowth) {
|
||||
setState("prefetchNoGrowth", 0)
|
||||
}
|
||||
|
||||
if (added <= 0) return
|
||||
if (growth <= 0) return
|
||||
|
||||
if (opts?.prefetch) {
|
||||
const current = turnStart()
|
||||
preserveScroll(() => setTurnStart(current + growth))
|
||||
if (growth > 0) {
|
||||
scheduleShiftReset()
|
||||
return
|
||||
}
|
||||
|
||||
if (turnStart() !== start) return
|
||||
|
||||
const currentRendered = renderedUserMessages().length
|
||||
const base = Math.max(beforeRendered, currentRendered)
|
||||
const target = Math.min(afterVisible, base + turnBatch)
|
||||
preserveScroll(() => setTurnStart(Math.max(0, afterVisible - target)))
|
||||
setState("shift", false)
|
||||
}
|
||||
|
||||
const loadAndReveal = () => fetchOlderMessages()
|
||||
|
||||
const onScrollerScroll = () => {
|
||||
if (!input.userScrolled()) return
|
||||
const el = input.scroller()
|
||||
if (!el) return
|
||||
if (el.scrollTop >= turnScrollThreshold) return
|
||||
|
||||
const start = turnStart()
|
||||
if (start > 0) {
|
||||
if (start <= turnPrefetchBuffer) {
|
||||
void fetchOlderMessages({ prefetch: true })
|
||||
}
|
||||
backfillTurns()
|
||||
return
|
||||
}
|
||||
if (el.scrollTop >= historyScrollThreshold) return
|
||||
|
||||
void fetchOlderMessages()
|
||||
}
|
||||
@@ -292,27 +161,18 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
on(
|
||||
input.sessionID,
|
||||
() => {
|
||||
setState({ prefetchUntil: 0, prefetchNoGrowth: 0 })
|
||||
cancelShiftReset()
|
||||
setState({ shift: false })
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionID(), input.messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
if (!id || !ready) return
|
||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
onCleanup(cancelShiftReset)
|
||||
|
||||
return {
|
||||
turnStart,
|
||||
setTurnStart,
|
||||
renderedUserMessages,
|
||||
userMessages,
|
||||
shift: () => state.shift,
|
||||
loadAndReveal,
|
||||
onScrollerScroll,
|
||||
}
|
||||
@@ -333,6 +193,7 @@ export default function Page() {
|
||||
const comments = useComments()
|
||||
const terminal = useTerminal()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
const location = useLocation()
|
||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||
|
||||
createEffect(() => {
|
||||
@@ -737,6 +598,7 @@ export default function Page() {
|
||||
let dockHeight = 0
|
||||
let scroller: HTMLDivElement | undefined
|
||||
let content: HTMLDivElement | undefined
|
||||
let revealMessage = (_id: string) => {}
|
||||
let scrollMark = 0
|
||||
let messageMark = 0
|
||||
|
||||
@@ -1403,9 +1265,8 @@ export default function Page() {
|
||||
},
|
||||
)
|
||||
|
||||
const historyWindow = createSessionHistoryWindow({
|
||||
const historyLoader = createSessionHistoryLoader({
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
loaded: () => messages().length,
|
||||
visibleUserMessages,
|
||||
historyMore,
|
||||
@@ -1427,9 +1288,9 @@ export default function Page() {
|
||||
const el = scroller
|
||||
if (!el) return
|
||||
if (el.scrollHeight > el.clientHeight + 1) return
|
||||
if (historyWindow.turnStart() <= 0 && !historyMore()) return
|
||||
if (!historyMore()) return
|
||||
|
||||
void historyWindow.loadAndReveal()
|
||||
void historyLoader.loadAndReveal()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1439,15 +1300,14 @@ export default function Page() {
|
||||
[
|
||||
params.id,
|
||||
messagesReady(),
|
||||
historyWindow.turnStart(),
|
||||
historyMore(),
|
||||
historyLoading(),
|
||||
autoScroll.userScrolled(),
|
||||
visibleUserMessages().length,
|
||||
] as const,
|
||||
([id, ready, start, more, loading, scrolled]) => {
|
||||
([id, ready, more, loading, scrolled]) => {
|
||||
if (!id || !ready || loading || scrolled) return
|
||||
if (start <= 0 && !more) return
|
||||
if (!more) return
|
||||
fill()
|
||||
},
|
||||
{ defer: true },
|
||||
@@ -1496,12 +1356,7 @@ export default function Page() {
|
||||
return out
|
||||
})
|
||||
|
||||
const busy = (sessionID: string) => {
|
||||
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
|
||||
return (sync.data.message[sessionID] ?? []).some(
|
||||
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||
)
|
||||
}
|
||||
const busy = (sessionID: string) => sync.data.session_working(sessionID)
|
||||
|
||||
const queuedFollowups = createMemo(() => {
|
||||
const id = params.id
|
||||
@@ -1754,15 +1609,14 @@ export default function Page() {
|
||||
historyMore,
|
||||
historyLoading,
|
||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
||||
turnStart: historyWindow.turnStart,
|
||||
currentMessageId: () => store.messageId,
|
||||
pendingMessage: () => ui.pendingMessage,
|
||||
setPendingMessage: (value) => setUi("pendingMessage", value),
|
||||
setActiveMessage,
|
||||
setTurnStart: historyWindow.setTurnStart,
|
||||
autoScroll,
|
||||
scroller: () => scroller,
|
||||
anchor,
|
||||
revealMessage: (id) => revealMessage(id),
|
||||
scheduleScrollState,
|
||||
consumePendingMessage: layout.pendingMessage.consume,
|
||||
})
|
||||
@@ -1792,6 +1646,8 @@ export default function Page() {
|
||||
if (fillFrame !== undefined) cancelAnimationFrame(fillFrame)
|
||||
})
|
||||
|
||||
useUsageExceededDialogs()
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
{sessionSync() ?? ""}
|
||||
@@ -1835,20 +1691,23 @@ export default function Page() {
|
||||
>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<Switch>
|
||||
<Match when={params.id && mobileChanges()}>
|
||||
<div class="relative h-full overflow-hidden">
|
||||
{reviewContent({
|
||||
diffStyle: "unified",
|
||||
classes: {
|
||||
root: "pb-8",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={params.id}>
|
||||
<Show when={messagesReady()}>
|
||||
<MessageTimeline
|
||||
mobileChanges={mobileChanges()}
|
||||
mobileFallback={reviewContent({
|
||||
diffStyle: "unified",
|
||||
classes: {
|
||||
root: "pb-8",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
actions={actions}
|
||||
scroll={ui.scroll}
|
||||
onResumeScroll={resumeScroll}
|
||||
@@ -1858,8 +1717,11 @@ export default function Page() {
|
||||
onMarkScrollGesture={markScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
onUserScroll={markUserScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onHistoryScroll={historyLoader.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
shouldAnchorBottom={() =>
|
||||
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
||||
}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
content = el
|
||||
@@ -1868,14 +1730,12 @@ export default function Page() {
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
void historyWindow.loadAndReveal()
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
historyShift={historyLoader.shift()}
|
||||
userMessages={historyLoader.userMessages()}
|
||||
anchor={anchor}
|
||||
setRevealMessage={(fn) => {
|
||||
revealMessage = fn
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
@@ -46,10 +47,12 @@ export function SessionComposerRegion(props: {
|
||||
setPromptDockRef: (el: HTMLDivElement) => void
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const layout = useLayout()
|
||||
const prompt = usePrompt()
|
||||
const language = useLanguage()
|
||||
const route = useSessionKey()
|
||||
const sync = useSync()
|
||||
const view = layout.view(route.sessionKey)
|
||||
|
||||
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
|
||||
const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
|
||||
@@ -207,6 +210,8 @@ export function SessionComposerRegion(props: {
|
||||
<SessionTodoDock
|
||||
sessionID={route.params.id}
|
||||
todos={props.state.todos()}
|
||||
collapsed={view.todoCollapsed.get()}
|
||||
onToggle={() => view.todoCollapsed.set(!view.todoCollapsed.get())}
|
||||
collapseLabel={language.t("session.todo.collapse")}
|
||||
expandLabel={language.t("session.todo.expand")}
|
||||
dockProgress={value()}
|
||||
|
||||
@@ -57,14 +57,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
||||
)
|
||||
|
||||
const status = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
|
||||
const busy = createMemo(() => status().type !== "idle")
|
||||
const live = createMemo(() => busy() || blocked())
|
||||
const live = createMemo(() => sync.data.session_working(params.id ?? "") || blocked())
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
responding: undefined as string | undefined,
|
||||
|
||||
@@ -469,7 +469,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<div data-slot="question-text" class="overflow-auto">
|
||||
{question()?.question}
|
||||
</div>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
</Show>
|
||||
|
||||
@@ -42,18 +42,17 @@ function dot(status: Todo["status"]) {
|
||||
export function SessionTodoDock(props: {
|
||||
sessionID?: string
|
||||
todos: Todo[]
|
||||
collapsed: boolean
|
||||
onToggle: () => void
|
||||
collapseLabel: string
|
||||
expandLabel: string
|
||||
dockProgress: number
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: false,
|
||||
height: 320,
|
||||
})
|
||||
|
||||
const toggle = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
const total = createMemo(() => props.todos.length)
|
||||
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
|
||||
const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
|
||||
@@ -72,7 +71,7 @@ export function SessionTodoDock(props: {
|
||||
)
|
||||
|
||||
const preview = createMemo(() => active()?.content ?? "")
|
||||
const collapse = useSpring(() => (store.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
||||
const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
||||
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
|
||||
const shut = createMemo(() => 1 - dock())
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
|
||||
@@ -107,11 +106,11 @@ export function SessionTodoDock(props: {
|
||||
class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onClick={props.onToggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
props.onToggle()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -148,7 +147,7 @@ export function SessionTodoDock(props: {
|
||||
>
|
||||
<TextReveal
|
||||
class="text-14-regular text-text-base cursor-default"
|
||||
text={store.collapsed ? preview() : undefined}
|
||||
text={props.collapsed ? preview() : undefined}
|
||||
duration={600}
|
||||
travel={25}
|
||||
edge={17}
|
||||
@@ -161,7 +160,7 @@ export function SessionTodoDock(props: {
|
||||
<div class="ml-auto">
|
||||
<IconButton
|
||||
data-action="session-todo-toggle-button"
|
||||
data-collapsed={store.collapsed ? "true" : "false"}
|
||||
data-collapsed={props.collapsed ? "true" : "false"}
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
@@ -172,16 +171,16 @@ export function SessionTodoDock(props: {
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
props.onToggle()
|
||||
}}
|
||||
aria-label={store.collapsed ? props.expandLabel : props.collapseLabel}
|
||||
aria-label={props.collapsed ? props.expandLabel : props.collapseLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-slot="session-todo-list"
|
||||
aria-hidden={store.collapsed || off()}
|
||||
aria-hidden={props.collapsed || off()}
|
||||
classList={{
|
||||
"pointer-events-none": hide() > 0.1,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||
import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { groupParts, PartGroup, renderable } from "@opencode-ai/ui/message-part"
|
||||
import { Data, Equal } from "effect"
|
||||
|
||||
export type SummaryDiff = SnapshotFileDiff & { file: string }
|
||||
|
||||
export type TimelineRowMap = {
|
||||
CommentStrip: {
|
||||
userMessageID: string
|
||||
previousUserMessage: boolean
|
||||
}
|
||||
UserMessage: {
|
||||
userMessageID: string
|
||||
anchor: boolean
|
||||
previousUserMessage: boolean
|
||||
}
|
||||
TurnDivider: {
|
||||
userMessageID: string
|
||||
label: "compaction" | "interrupted"
|
||||
}
|
||||
AssistantPart: {
|
||||
userMessageID: string
|
||||
group: PartGroup
|
||||
previousAssistantPart: boolean
|
||||
lastAssistantPart: boolean
|
||||
}
|
||||
Thinking: { userMessageID: string; reasoningHeading?: string }
|
||||
Retry: { userMessageID: string }
|
||||
DiffSummary: { userMessageID: string; diffs: SummaryDiff[] }
|
||||
Error: { userMessageID: string; text: string }
|
||||
BottomSpacer: {}
|
||||
}
|
||||
|
||||
export namespace TimelineRow {
|
||||
export class CommentStrip extends Data.TaggedClass("CommentStrip")<{
|
||||
userMessageID: string
|
||||
previousUserMessage: boolean
|
||||
}> {}
|
||||
export class UserMessage extends Data.TaggedClass("UserMessage")<{
|
||||
userMessageID: string
|
||||
anchor: boolean
|
||||
previousUserMessage: boolean
|
||||
}> {}
|
||||
export class TurnDivider extends Data.TaggedClass("TurnDivider")<{
|
||||
userMessageID: string
|
||||
label: "compaction" | "interrupted"
|
||||
}> {}
|
||||
export class AssistantPart extends Data.TaggedClass("AssistantPart")<{
|
||||
userMessageID: string
|
||||
group: PartGroup
|
||||
previousAssistantPart: boolean
|
||||
lastAssistantPart: boolean
|
||||
}> {}
|
||||
export class Thinking extends Data.TaggedClass("Thinking")<{
|
||||
userMessageID: string
|
||||
reasoningHeading?: string
|
||||
}> {}
|
||||
export class DiffSummary extends Data.TaggedClass("DiffSummary")<{
|
||||
userMessageID: string
|
||||
diffs: SummaryDiff[]
|
||||
}> {}
|
||||
export class Error extends Data.TaggedClass("Error")<{
|
||||
userMessageID: string
|
||||
text: string
|
||||
}> {}
|
||||
export class Retry extends Data.TaggedClass("Retry")<{
|
||||
userMessageID: string
|
||||
}> {}
|
||||
export class BottomSpacer extends Data.TaggedClass("BottomSpacer")<{}> {}
|
||||
|
||||
export type TimelineRow =
|
||||
| CommentStrip
|
||||
| UserMessage
|
||||
| TurnDivider
|
||||
| AssistantPart
|
||||
| Thinking
|
||||
| DiffSummary
|
||||
| Error
|
||||
| Retry
|
||||
| BottomSpacer
|
||||
|
||||
export const key = (row: TimelineRow) => {
|
||||
switch (row._tag) {
|
||||
case "CommentStrip":
|
||||
return `comment-strip:${row.userMessageID}`
|
||||
case "UserMessage":
|
||||
return `user-message:${row.userMessageID}`
|
||||
case "TurnDivider":
|
||||
return `turn-divider:${row.userMessageID}:${row.label}`
|
||||
case "AssistantPart":
|
||||
return `assistant-part:${row.userMessageID}:${row.group.key}`
|
||||
case "Thinking":
|
||||
return `thinking:${row.userMessageID}`
|
||||
case "DiffSummary":
|
||||
return `diff-summary:${row.userMessageID}`
|
||||
case "Error":
|
||||
return `error:${row.userMessageID}`
|
||||
case "Retry":
|
||||
return `retry:${row.userMessageID}`
|
||||
case "BottomSpacer":
|
||||
return "bottom-spacer"
|
||||
}
|
||||
}
|
||||
|
||||
export function equals(a: TimelineRow, b: TimelineRow) {
|
||||
return Equal.equals(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Timeline {
|
||||
export function constructMessageRows(
|
||||
userMessage: UserMessage,
|
||||
getMessageParts: (messageID: string) => Part[],
|
||||
assistantMessages: AssistantMessage[],
|
||||
index: number,
|
||||
showReasoning: boolean,
|
||||
status: SessionStatus["type"],
|
||||
isActive: boolean,
|
||||
) {
|
||||
const rows: TimelineRow.TimelineRow[] = []
|
||||
|
||||
const previousUserMessage = index > 0
|
||||
const userParts = getMessageParts(userMessage.id)
|
||||
const comments = userParts.flatMap((p) => MessageComment.fromPart(p) ?? [])
|
||||
const compaction = userParts.some((p) => p.type === "compaction")
|
||||
const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError")
|
||||
const interrupted = interruptedMessageIndex !== -1
|
||||
const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error
|
||||
|
||||
const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) =>
|
||||
getMessageParts(message.id)
|
||||
.filter((part) => renderable(part, showReasoning))
|
||||
.map((part) => ({ messageID: message.id, messageIndex, part })),
|
||||
)
|
||||
const assistantItems =
|
||||
interrupted && !compaction
|
||||
? [
|
||||
...groupParts(assistantPartRefs.filter((ref) => ref.messageIndex <= interruptedMessageIndex)).map(
|
||||
(group) => ({
|
||||
type: "part" as const,
|
||||
group,
|
||||
}),
|
||||
),
|
||||
{ type: "interrupted" as const },
|
||||
...groupParts(assistantPartRefs.filter((ref) => ref.messageIndex > interruptedMessageIndex)).map(
|
||||
(group) => ({
|
||||
type: "part" as const,
|
||||
group,
|
||||
}),
|
||||
),
|
||||
]
|
||||
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
|
||||
const assistantGroupCount = assistantItems.filter((item) => item.type === "part").length
|
||||
|
||||
if (comments.length > 0)
|
||||
rows.push(
|
||||
new TimelineRow.CommentStrip({
|
||||
userMessageID: userMessage.id,
|
||||
previousUserMessage,
|
||||
}),
|
||||
)
|
||||
|
||||
rows.push(
|
||||
new TimelineRow.UserMessage({
|
||||
userMessageID: userMessage.id,
|
||||
anchor: comments.length === 0,
|
||||
previousUserMessage: comments.length === 0 && previousUserMessage,
|
||||
}),
|
||||
)
|
||||
|
||||
if (compaction) {
|
||||
rows.push(
|
||||
new TimelineRow.TurnDivider({
|
||||
userMessageID: userMessage.id,
|
||||
label: "compaction",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
let assistantGroupIndex = 0
|
||||
assistantItems.forEach((item) => {
|
||||
if (item.type === "interrupted") {
|
||||
rows.push(
|
||||
new TimelineRow.TurnDivider({
|
||||
userMessageID: userMessage.id,
|
||||
label: "interrupted",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
rows.push(
|
||||
new TimelineRow.AssistantPart({
|
||||
userMessageID: userMessage.id,
|
||||
group: item.group,
|
||||
previousAssistantPart: assistantGroupIndex > 0,
|
||||
lastAssistantPart: assistantGroupIndex === assistantGroupCount - 1,
|
||||
}),
|
||||
)
|
||||
assistantGroupIndex += 1
|
||||
})
|
||||
|
||||
if (isActive && status === "busy" && !error && (showReasoning ? assistantPartRefs.length === 0 : true)) {
|
||||
const heading = assistantMessages
|
||||
.flatMap((message) => getMessageParts(message.id))
|
||||
.map((part) => (part.type === "reasoning" && part.text ? reasoningHeading(part.text) : undefined))
|
||||
.find((value): value is string => !!value)
|
||||
|
||||
rows.push(
|
||||
new TimelineRow.Thinking({
|
||||
userMessageID: userMessage.id,
|
||||
reasoningHeading: heading,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (isActive && status === "retry") rows.push(new TimelineRow.Retry({ userMessageID: userMessage.id }))
|
||||
|
||||
const diffs = (userMessage.summary?.diffs ?? [])
|
||||
.reduceRight<SummaryDiff[]>((result, diff) => {
|
||||
if (!isSummaryDiff(diff)) return result
|
||||
if (result.some((item) => item.file === diff.file)) return result
|
||||
result.push(diff)
|
||||
return result
|
||||
}, [])
|
||||
.reverse()
|
||||
if (diffs.length > 0 && (status === "idle" || !isActive)) {
|
||||
rows.push(
|
||||
new TimelineRow.DiffSummary({
|
||||
userMessageID: userMessage.id,
|
||||
diffs,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const data = error.data?.message
|
||||
rows.push(
|
||||
new TimelineRow.Error({
|
||||
userMessageID: userMessage.id,
|
||||
text: unwrapErrorMessage(
|
||||
typeof data === "string" ? data : data === undefined || data === null ? "" : String(data),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff {
|
||||
return typeof value.file === "string"
|
||||
}
|
||||
|
||||
function reasoningHeading(text: string) {
|
||||
const markdown = text.replace(/\r\n?/g, "\n")
|
||||
const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i)
|
||||
if (html?.[1]) {
|
||||
const value = cleanHeading(html[1].replace(/<[^>]+>/g, " "))
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m)
|
||||
if (atx?.[1]) {
|
||||
const value = cleanHeading(atx[1])
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m)
|
||||
if (setext?.[1]) {
|
||||
const value = cleanHeading(setext[1])
|
||||
if (value) return value
|
||||
}
|
||||
|
||||
const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m)
|
||||
if (strong?.[1]) {
|
||||
const value = cleanHeading(strong[1])
|
||||
if (value) return value
|
||||
}
|
||||
}
|
||||
|
||||
function cleanHeading(value: string) {
|
||||
return value
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/[*_~]+/g, "")
|
||||
.trim()
|
||||
}
|
||||
|
||||
function unwrapErrorMessage(message: string) {
|
||||
const text = message.replace(/^Error:\s*/, "").trim()
|
||||
|
||||
const parse = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const read = (value: string) => {
|
||||
const first = parse(value)
|
||||
if (typeof first !== "string") return first
|
||||
return parse(first.trim())
|
||||
}
|
||||
|
||||
let json = read(text)
|
||||
|
||||
if (json === undefined) {
|
||||
const start = text.indexOf("{")
|
||||
const end = text.lastIndexOf("}")
|
||||
if (start !== -1 && end > start) json = read(text.slice(start, end + 1))
|
||||
}
|
||||
|
||||
if (!record(json)) return message
|
||||
|
||||
const err = record(json.error) ? json.error : undefined
|
||||
if (err) {
|
||||
const type = typeof err.type === "string" ? err.type : undefined
|
||||
const msg = typeof err.message === "string" ? err.message : undefined
|
||||
if (type && msg) return `${type}: ${msg}`
|
||||
if (msg) return msg
|
||||
if (type) return type
|
||||
const code = typeof err.code === "string" ? err.code : undefined
|
||||
if (code) return code
|
||||
}
|
||||
|
||||
const msg = typeof json.message === "string" ? json.message : undefined
|
||||
if (msg) return msg
|
||||
|
||||
const reason = typeof json.error === "string" ? json.error : undefined
|
||||
if (reason) return reason
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MessageComment {
|
||||
export type MessageComment = {
|
||||
path: string
|
||||
comment: string
|
||||
selection?: {
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
}
|
||||
|
||||
export const fromPart = (part: Part): MessageComment | undefined => {
|
||||
if (part.type !== "text" || !part.synthetic) return
|
||||
const next = readCommentMetadata(part.metadata) ?? parseCommentNote(part.text)
|
||||
if (!next) return
|
||||
return {
|
||||
path: next.path,
|
||||
comment: next.comment,
|
||||
selection: next.selection
|
||||
? {
|
||||
startLine: next.selection.startLine,
|
||||
endLine: next.selection.endLine,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ export interface SessionReviewTabProps {
|
||||
focusedComment?: { file: string; id: string } | null
|
||||
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
|
||||
focusedFile?: string
|
||||
onScrollRef?: (el: HTMLDivElement) => void
|
||||
onScrollRef?: (el: HTMLDivElement | undefined) => void
|
||||
commentMentions?: {
|
||||
items: (query: string) => string[] | Promise<string[]>
|
||||
}
|
||||
@@ -126,6 +126,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
|
||||
onCleanup(() => {
|
||||
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
|
||||
props.onScrollRef?.(undefined)
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,12 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
|
||||
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
|
||||
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
||||
return typeof value.file === "string"
|
||||
}
|
||||
|
||||
export function SessionSidePanel(props: {
|
||||
canReview: () => boolean
|
||||
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
|
||||
@@ -70,7 +76,8 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
||||
|
||||
const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
|
||||
const diffs = createMemo(() => props.diffs().filter(renderDiff))
|
||||
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
||||
const kinds = createMemo(() => {
|
||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||
if (!a) return b
|
||||
@@ -81,7 +88,7 @@ export function SessionSidePanel(props: {
|
||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||
|
||||
const out = new Map<string, "add" | "del" | "mix">()
|
||||
for (const diff of props.diffs()) {
|
||||
for (const diff of diffs()) {
|
||||
const file = normalize(diff.file)
|
||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||
|
||||
@@ -214,239 +221,241 @@ export function SessionSidePanel(props: {
|
||||
}}
|
||||
style={{ width: panelWidth() }}
|
||||
>
|
||||
<div class="size-full flex border-l border-border-weaker-base">
|
||||
<div
|
||||
aria-hidden={!reviewOpen()}
|
||||
inert={!reviewOpen()}
|
||||
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
|
||||
classList={{
|
||||
"pointer-events-none": !reviewOpen(),
|
||||
}}
|
||||
>
|
||||
<div class="size-full min-w-0 h-full bg-background-base">
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={activeTab()} onChange={openTab}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
const stop = createFileTabListSync({ el, contextOpen })
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Tabs.Trigger value="review">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{language.t("session.tab.review")}</div>
|
||||
<Show when={props.hasReview()}>
|
||||
<div>{props.reviewCount()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={contextOpen()}>
|
||||
<Tabs.Trigger
|
||||
value="context"
|
||||
closeButton={
|
||||
<TooltipKeybind
|
||||
title={language.t("common.closeTab")}
|
||||
keybind={command.keybind("tab.close")}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => tabs().close("context")}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
hideCloseButton
|
||||
onMiddleClick={() => tabs().close("context")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage variant="indicator" />
|
||||
<div>{language.t("session.tab.context")}</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={openedTabs()}>
|
||||
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.file.open")}
|
||||
keybind={command.keybind("file.open")}
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
class="!rounded-md"
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-file").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||
})
|
||||
}}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={activeTab() === "empty"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">
|
||||
{language.t("session.files.selectToOpen")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
|
||||
<Show when={contextOpen()}>
|
||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={activeTab() === "context"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionContextTab />
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={activeFileTab()} keyed>
|
||||
{(tab) => <FileTabContent tab={tab} />}
|
||||
</Show>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable} keyed>
|
||||
{(tab) => {
|
||||
const path = file.pathFromTab(tab)
|
||||
return (
|
||||
<div data-component="tabs-drag-preview">
|
||||
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={shown()}>
|
||||
<Show when={open()}>
|
||||
<div class="size-full flex border-l border-border-weaker-base">
|
||||
<div
|
||||
id="file-tree-panel"
|
||||
aria-hidden={!fileOpen()}
|
||||
inert={!fileOpen()}
|
||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||
aria-hidden={!reviewOpen()}
|
||||
inert={!reviewOpen()}
|
||||
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
|
||||
classList={{
|
||||
"pointer-events-none": !fileOpen(),
|
||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!props.size.active(),
|
||||
"pointer-events-none": !reviewOpen(),
|
||||
}}
|
||||
style={{ width: treeWidth() }}
|
||||
>
|
||||
<div
|
||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||
>
|
||||
<Tabs
|
||||
variant="pill"
|
||||
value={fileTreeTab()}
|
||||
onChange={setFileTreeTabValue}
|
||||
class="h-full"
|
||||
data-scope="filetree"
|
||||
<div class="size-full min-w-0 h-full bg-background-base">
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{props.reviewCount()}{" "}
|
||||
{language.t(
|
||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{language.t("session.files.all")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={activeTab()} onChange={openTab}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
const stop = createFileTabListSync({ el, contextOpen })
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Tabs.Trigger value="review">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{language.t("session.tab.review")}</div>
|
||||
<Show when={props.hasReview()}>
|
||||
<div>{props.reviewCount()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={contextOpen()}>
|
||||
<Tabs.Trigger
|
||||
value="context"
|
||||
closeButton={
|
||||
<TooltipKeybind
|
||||
title={language.t("common.closeTab")}
|
||||
keybind={command.keybind("tab.close")}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => tabs().close("context")}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
hideCloseButton
|
||||
onMiddleClick={() => tabs().close("context")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage variant="indicator" />
|
||||
<div>{language.t("session.tab.context")}</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={openedTabs()}>
|
||||
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.file.open")}
|
||||
keybind={command.keybind("file.open")}
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
class="!rounded-md"
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-file").then((x) => {
|
||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||
})
|
||||
}}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={reviewOpen() && activeTab() === "review"}>{props.reviewPanel()}</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={activeTab() === "empty"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">
|
||||
{language.t("session.files.selectToOpen")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
|
||||
<Show when={contextOpen()}>
|
||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={activeTab() === "context"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionContextTab />
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={activeFileTab()} keyed>
|
||||
{(tab) => <FileTabContent tab={tab} />}
|
||||
</Show>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable} keyed>
|
||||
{(tab) => {
|
||||
const path = file.pathFromTab(tab)
|
||||
return (
|
||||
<div data-component="tabs-drag-preview">
|
||||
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={shown()}>
|
||||
<div
|
||||
id="file-tree-panel"
|
||||
aria-hidden={!fileOpen()}
|
||||
inert={!fileOpen()}
|
||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||
classList={{
|
||||
"pointer-events-none": !fileOpen(),
|
||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||
!props.size.active(),
|
||||
}}
|
||||
style={{ width: treeWidth() }}
|
||||
>
|
||||
<div
|
||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||
>
|
||||
<Tabs
|
||||
variant="pill"
|
||||
value={fileTreeTab()}
|
||||
onChange={setFileTreeTabValue}
|
||||
class="h-full"
|
||||
data-scope="filetree"
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{props.reviewCount()}{" "}
|
||||
{language.t(
|
||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||
{language.t("session.files.all")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||
<Match when={true}>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
modified={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||
<Match when={true}>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
modified={diffFiles()}
|
||||
kinds={kinds()}
|
||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Show when={fileOpen()}>
|
||||
<div onPointerDown={() => props.size.start()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
edge="start"
|
||||
size={layout.fileTree.width()}
|
||||
min={200}
|
||||
max={480}
|
||||
onResize={(width) => {
|
||||
props.size.touch()
|
||||
layout.fileTree.resize(width)
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={fileOpen()}>
|
||||
<div onPointerDown={() => props.size.start()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
edge="start"
|
||||
size={layout.fileTree.width()}
|
||||
min={200}
|
||||
max={480}
|
||||
onResize={(width) => {
|
||||
props.size.touch()
|
||||
layout.fileTree.resize(width)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</aside>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { SessionStatus } from "@opencode-ai/sdk/v2"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useSessionLayout } from "./session-layout"
|
||||
import { useDialog } from "@opencode-ai/ui/context"
|
||||
import { DialogUsageExceeded } from "@/components/dialog-usage-exceeded"
|
||||
import { useI18n } from "@opencode-ai/ui/context"
|
||||
|
||||
const GO_UPSELL_FREE_TIER_LAST_SEEN_AT = "go_upsell_last_seen_at"
|
||||
const GO_UPSELL_FREE_TIER_DONT_SHOW = "go_upsell_dont_show"
|
||||
const GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT = "go_upsell_account_rate_limit_last_seen_at"
|
||||
const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_dont_show"
|
||||
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
|
||||
const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
|
||||
|
||||
function goUpsellKeys(status: SessionStatus) {
|
||||
if (status.type !== "retry" || !status.action) return
|
||||
const { action } = status
|
||||
if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
|
||||
if (action.reason === "free_tier_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
|
||||
} as const
|
||||
}
|
||||
if (action.reason === "account_rate_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
|
||||
export function useUsageExceededDialogs() {
|
||||
const sdk = useSDK()
|
||||
const dialog = useDialog()
|
||||
const { params } = useSessionLayout()
|
||||
const { t, locale } = useI18n()
|
||||
const isEnglish = () => locale() === "en"
|
||||
|
||||
const [goUpsellState, setGoUpsellState] = persisted(
|
||||
Persist.global("go-upsell"),
|
||||
createStore({
|
||||
[GO_UPSELL_FREE_TIER_LAST_SEEN_AT]: null as null | number,
|
||||
[GO_UPSELL_FREE_TIER_DONT_SHOW]: null as null | number,
|
||||
[GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT]: null as null | number,
|
||||
[GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW]: null as null | number,
|
||||
}),
|
||||
)
|
||||
|
||||
onCleanup(
|
||||
sdk.event.on("session.status", (evt) => {
|
||||
if (evt.properties.sessionID !== params.id) return
|
||||
if (evt.properties.status.type !== "retry") return
|
||||
const { action } = evt.properties.status
|
||||
if (!action) return
|
||||
if (dialog.active) return
|
||||
|
||||
const keys = goUpsellKeys(evt.properties.status)
|
||||
if (!keys) return
|
||||
|
||||
const seen = goUpsellState[keys.lastSeenAt]
|
||||
if (seen && Date.now() - seen < GO_UPSELL_WINDOW) return
|
||||
if (goUpsellState[keys.dontShow]) return
|
||||
|
||||
if (action.reason === "free_tier_limit") {
|
||||
dialog.show(() => (
|
||||
<DialogUsageExceeded
|
||||
title={isEnglish() ? action.title : t("dialog.usageExceeded.freeTier.title")}
|
||||
description={isEnglish() ? action.message : t("dialog.usageExceeded.freeTier.description")}
|
||||
actionLabel={isEnglish() ? action.label : t("dialog.usageExceeded.freeTier.actionLabel")}
|
||||
link={action.link}
|
||||
onClose={(dontShowAgain) => {
|
||||
setGoUpsellState(keys.lastSeenAt, Date.now())
|
||||
if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now())
|
||||
else {
|
||||
void import("../../components/dialog-connect-provider").then((x) =>
|
||||
dialog.show(() => <x.DialogConnectProvider provider="opencode-go" />),
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))
|
||||
} else if (action.reason === "account_rate_limit") {
|
||||
dialog.show(() => (
|
||||
<DialogUsageExceeded
|
||||
title={isEnglish() ? action.title : t("dialog.usageExceeded.accountRateLimit.title")}
|
||||
description={isEnglish() ? action.message : t("dialog.usageExceeded.accountRateLimit.description")}
|
||||
actionLabel={isEnglish() ? action.label : t("dialog.usageExceeded.accountRateLimit.actionLabel")}
|
||||
link={action.link}
|
||||
onClose={(dontShowAgain) => {
|
||||
setGoUpsellState(keys.lastSeenAt, Date.now())
|
||||
if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now())
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -75,8 +75,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||
settings.general.showFileTree()
|
||||
|
||||
const idle = { type: "idle" as const }
|
||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||
const messages = () => {
|
||||
const id = params.id
|
||||
if (!id) return []
|
||||
@@ -290,7 +288,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
if (status().type !== "idle") {
|
||||
if (sync.data.session_working(params.id ?? "")) {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,19 @@ export const useSessionHashScroll = (input: {
|
||||
historyMore: () => boolean
|
||||
historyLoading: () => boolean
|
||||
loadMore: (sessionID: string) => Promise<void>
|
||||
turnStart: () => number
|
||||
currentMessageId: () => string | undefined
|
||||
pendingMessage: () => string | undefined
|
||||
setPendingMessage: (value: string | undefined) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
setTurnStart: (value: number) => void
|
||||
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
anchor: (id: string) => string
|
||||
revealMessage?: (id: string) => void
|
||||
scheduleScrollState: (el: HTMLDivElement) => void
|
||||
consumePendingMessage: (key: string) => string | undefined
|
||||
}) => {
|
||||
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
|
||||
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
||||
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
||||
let pendingKey = ""
|
||||
let clearing = false
|
||||
|
||||
@@ -77,6 +75,7 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
|
||||
const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => {
|
||||
input.revealMessage?.(id)
|
||||
const el = document.getElementById(input.anchor(id))
|
||||
if (el) return scrollToElement(el, behavior)
|
||||
if (left <= 0) return false
|
||||
@@ -89,18 +88,7 @@ export const useSessionHashScroll = (input: {
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
cancel()
|
||||
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
||||
|
||||
const index = messageIndex().get(message.id) ?? -1
|
||||
if (index !== -1 && index < input.turnStart()) {
|
||||
input.setTurnStart(index)
|
||||
|
||||
queue(() => {
|
||||
seek(message.id, behavior)
|
||||
})
|
||||
|
||||
updateHash(message.id)
|
||||
return
|
||||
}
|
||||
input.revealMessage?.(message.id)
|
||||
|
||||
if (seek(message.id, behavior)) {
|
||||
updateHash(message.id)
|
||||
@@ -154,7 +142,6 @@ export const useSessionHashScroll = (input: {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
|
||||
visibleUserMessages()
|
||||
input.turnStart()
|
||||
|
||||
let targetId = input.pendingMessage()
|
||||
if (!targetId) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import z from "zod"
|
||||
|
||||
const prefixes = {
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
@@ -15,10 +13,6 @@ let counter = 0
|
||||
|
||||
type Prefix = keyof typeof prefixes
|
||||
export namespace Identifier {
|
||||
export function schema(prefix: Prefix) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
|
||||
export function ascending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, false, given)
|
||||
}
|
||||
|
||||
@@ -128,4 +128,17 @@ describe("formatServerError", () => {
|
||||
["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
test("unwraps SDK-wrapped errors from cause.body", () => {
|
||||
const body = {
|
||||
name: "ConfigInvalidError",
|
||||
data: {
|
||||
message: "Missing host",
|
||||
},
|
||||
} satisfies ConfigInvalidError
|
||||
|
||||
const wrapped = new Error("ConfigInvalidError", { cause: { body, status: 400 } })
|
||||
|
||||
expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,14 +26,22 @@ function tr(translator: Translator | undefined, key: string, text: string, vars?
|
||||
}
|
||||
|
||||
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
|
||||
if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
|
||||
if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
|
||||
const unwrapped = unwrapNamedError(error)
|
||||
if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
|
||||
if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (typeof error === "string" && error) return error
|
||||
if (fallback) return fallback
|
||||
return tr(translate, "error.chain.unknown", "Unknown error")
|
||||
}
|
||||
|
||||
function unwrapNamedError(error: unknown): unknown {
|
||||
if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
|
||||
return (error.cause as Record<string, unknown>).body
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
|
||||
if (typeof error !== "object" || error === null) return false
|
||||
const o = error as Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.39",
|
||||
"version": "1.15.5",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -31,11 +31,11 @@
|
||||
"solid-js": "catalog:",
|
||||
"solid-list": "0.3.0",
|
||||
"solid-stripe": "0.8.1",
|
||||
"svix": "1.92.2",
|
||||
"vite": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "150K",
|
||||
full: "150,000",
|
||||
compact: "160K",
|
||||
full: "160,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "850",
|
||||
commits: "11,000",
|
||||
monthlyUsers: "6.5M",
|
||||
contributors: "900",
|
||||
commits: "13,000",
|
||||
monthlyUsers: "7.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -249,7 +249,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.meta.description":
|
||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.hero.body":
|
||||
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
||||
@@ -261,8 +261,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 للشهر الأول",
|
||||
"go.pricing.body":
|
||||
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: حد الاستخدام 3 أضعاف حتى 27 أبريل",
|
||||
"go.graph.free": "مجاني",
|
||||
"go.graph.freePill": "Big Pickle ونماذج مجانية",
|
||||
"go.graph.go": "Go",
|
||||
@@ -300,7 +298,7 @@ export const dict = {
|
||||
"go.problem.item2": "حدود سخية ووصول موثوق",
|
||||
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
||||
"go.problem.item4":
|
||||
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash",
|
||||
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash",
|
||||
"go.how.title": "كيف يعمل Go",
|
||||
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
||||
"go.how.step1.title": "أنشئ حسابًا",
|
||||
@@ -324,7 +322,7 @@ export const dict = {
|
||||
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
|
||||
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
||||
"go.faq.a3":
|
||||
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||
"go.faq.q4": "كم تكلفة Go؟",
|
||||
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
||||
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
|
||||
@@ -347,7 +345,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
||||
"go.faq.a9":
|
||||
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
||||
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||
@@ -357,8 +355,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "مفتاح API مفقود.",
|
||||
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"تم تجاوز حصة الاشتراك. يمكنك الاستمرار في استخدام النماذج المجانية.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"تم الوصول إلى حد الاستخدام الأسبوعي. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"تم الوصول إلى حد الاستخدام الشهري. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "لا توجد طريقة دفع. أضف طريقة دفع هنا: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "رصيد غير كاف. إدارة فواتيرك هنا: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||
"go.meta.description":
|
||||
"O Go 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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||
"go.hero.body":
|
||||
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
||||
@@ -265,8 +265,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 no primeiro mês",
|
||||
"go.pricing.body":
|
||||
"Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: limite de uso 3x maior até 27 de abril",
|
||||
"go.graph.free": "Grátis",
|
||||
"go.graph.freePill": "Big Pickle e modelos gratuitos",
|
||||
"go.graph.go": "Go",
|
||||
@@ -305,7 +303,7 @@ export const dict = {
|
||||
"go.problem.item2": "Limites generosos e acesso confiável",
|
||||
"go.problem.item3": "Feito para o maior número possível de programadores",
|
||||
"go.problem.item4":
|
||||
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||
"go.how.title": "Como o Go funciona",
|
||||
"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.",
|
||||
@@ -331,7 +329,7 @@ export const dict = {
|
||||
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
|
||||
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Quanto custa o Go?",
|
||||
"go.faq.a4.p1.beforePricing": "O Go custa",
|
||||
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
|
||||
@@ -355,7 +353,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash 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.modelNotSupported": "Modelo {{model}} não suportado",
|
||||
@@ -365,8 +363,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Chave de API ausente.",
|
||||
"zen.api.error.invalidApiKey": "Chave de API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cota de assinatura excedida. Você pode continuar usando modelos gratuitos.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Limite de uso de 5 horas atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Limite de uso semanal atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Limite de uso mensal atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Nenhuma forma de pagamento. Adicione uma forma de pagamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gerencie seu faturamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -251,7 +251,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||
"go.hero.body":
|
||||
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
||||
@@ -263,8 +263,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 første måned",
|
||||
"go.pricing.body":
|
||||
"Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: brugsgrænsen tredoblet til 27. april",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle og gratis modeller",
|
||||
"go.graph.go": "Go",
|
||||
@@ -302,7 +300,7 @@ export const dict = {
|
||||
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
||||
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
||||
"go.problem.item4":
|
||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||
"go.how.title": "Hvordan Go virker",
|
||||
"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.",
|
||||
@@ -328,7 +326,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
|
||||
"go.faq.q3": "Er Go det samme som Zen?",
|
||||
"go.faq.a3":
|
||||
"Nej. Zen er pay-as-you-go, mens Go 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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Hvad koster Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
||||
@@ -351,7 +349,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash 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.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||
@@ -361,8 +359,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Manglende API-nøgle.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøgle.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsætte med at bruge gratis modeller.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Forbrugsgrænsen for 5 timer er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Ugentlig forbrugsgrænse er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Månedlig forbrugsgrænse er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Tilføj en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrækkelig saldo. Administrer din fakturering her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||
"go.hero.body":
|
||||
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
||||
@@ -265,8 +265,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 im ersten Monat",
|
||||
"go.pricing.body":
|
||||
"Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: Nutzungslimit bis zum 27. April verdreifacht",
|
||||
"go.graph.free": "Kostenlos",
|
||||
"go.graph.freePill": "Big Pickle und kostenlose Modelle",
|
||||
"go.graph.go": "Go",
|
||||
@@ -304,7 +302,7 @@ export const dict = {
|
||||
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
||||
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
||||
"go.problem.item4":
|
||||
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash",
|
||||
"Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash",
|
||||
"go.how.title": "Wie Go funktioniert",
|
||||
"go.how.body":
|
||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
|
||||
@@ -330,7 +328,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.",
|
||||
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Wie viel kostet Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go kostet",
|
||||
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
|
||||
@@ -354,7 +352,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash 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.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||
@@ -364,8 +362,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Fehlender API-Key.",
|
||||
"zen.api.error.invalidApiKey": "Ungültiger API-Key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnement-Quote überschritten. Du kannst weiterhin kostenlose Modelle nutzen.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5-Stunden-Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Wöchentliches Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Monatliches Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Keine Zahlungsmethode. Füge hier eine Zahlungsmethode hinzu: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Unzureichendes Guthaben. Verwalte deine Abrechnung hier: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -248,9 +248,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6 gets 3× usage limits through April 27",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Low cost coding models for everyone",
|
||||
"go.hero.body":
|
||||
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
|
||||
@@ -298,7 +296,7 @@ export const dict = {
|
||||
"go.problem.item2": "Generous limits and reliable access",
|
||||
"go.problem.item3": "Built for as many programmers as possible",
|
||||
"go.problem.item4":
|
||||
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash",
|
||||
"Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash",
|
||||
"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.step1.title": "Create an account",
|
||||
@@ -323,7 +321,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.",
|
||||
"go.faq.q3": "Is Go the same as Zen?",
|
||||
"go.faq.a3":
|
||||
"No. Zen is pay-as-you-go, while Go 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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "How much does Go cost?",
|
||||
"go.faq.a4.p1.beforePricing": "Go costs",
|
||||
"go.faq.a4.p1.pricingLink": "$5 first month",
|
||||
@@ -347,7 +345,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "What is the difference between free models and Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash 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.modelNotSupported": "Model {{model}} not supported",
|
||||
@@ -357,8 +355,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Missing API key.",
|
||||
"zen.api.error.invalidApiKey": "Invalid API key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Subscription quota exceeded. You can continue using free models.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Weekly usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Monthly usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "No payment method. Add a payment method here: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Insufficient balance. Manage your billing here: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -254,7 +254,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||
"go.hero.body":
|
||||
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
|
||||
@@ -266,8 +266,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 el primer mes",
|
||||
"go.pricing.body":
|
||||
"Úsalo con cualquier agente. $5 el primer mes, luego 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: límite de uso triplicado hasta el 27 de abril",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle y modelos gratuitos",
|
||||
"go.graph.go": "Go",
|
||||
@@ -306,7 +304,7 @@ export const dict = {
|
||||
"go.problem.item2": "Límites generosos y acceso fiable",
|
||||
"go.problem.item3": "Creado para tantos programadores como sea posible",
|
||||
"go.problem.item4":
|
||||
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash",
|
||||
"Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash",
|
||||
"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.step1.title": "Crear una cuenta",
|
||||
@@ -331,7 +329,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.",
|
||||
"go.faq.q3": "¿Es Go lo mismo que Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "¿Cuánto cuesta Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go cuesta",
|
||||
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
|
||||
@@ -355,7 +353,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash 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.modelNotSupported": "Modelo {{model}} no soportado",
|
||||
@@ -365,8 +363,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Falta la clave API.",
|
||||
"zen.api.error.invalidApiKey": "Clave API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cuota de suscripción excedida. Puedes continuar usando modelos gratuitos.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Límite de uso de 5 horas alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Límite de uso semanal alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Límite de uso mensual alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Sin método de pago. Añade un método de pago aquí: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gestiona tu facturación aquí: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -255,7 +255,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||
"go.hero.body":
|
||||
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
|
||||
@@ -267,8 +267,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 le premier mois",
|
||||
"go.pricing.body":
|
||||
"Utilisez-le avec n'importe quel agent. $5 le premier mois, puis 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6 : limites d’utilisation triplées jusqu’au 27 avril",
|
||||
"go.graph.free": "Gratuit",
|
||||
"go.graph.freePill": "Big Pickle et modèles gratuits",
|
||||
"go.graph.go": "Go",
|
||||
@@ -306,7 +304,7 @@ export const dict = {
|
||||
"go.problem.item2": "Limites généreuses et accès fiable",
|
||||
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
|
||||
"go.problem.item4":
|
||||
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash",
|
||||
"Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash",
|
||||
"go.how.title": "Comment fonctionne Go",
|
||||
"go.how.body":
|
||||
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
|
||||
@@ -332,7 +330,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.",
|
||||
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
|
||||
"go.faq.a3":
|
||||
"Non. Zen est 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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Combien coûte Go ?",
|
||||
"go.faq.a4.p1.beforePricing": "Go coûte",
|
||||
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
|
||||
@@ -355,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é.",
|
||||
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash 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.modelNotSupported": "Modèle {{model}} non pris en charge",
|
||||
@@ -365,8 +363,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Clé API manquante.",
|
||||
"zen.api.error.invalidApiKey": "Clé API invalide.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota d'abonnement dépassé. Vous pouvez continuer à utiliser les modèles gratuits.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Limite d'utilisation sur 5 heures atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Limite d'utilisation hebdomadaire atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Limite d'utilisation mensuelle atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Aucune méthode de paiement. Ajoutez une méthode de paiement ici : {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Solde insuffisant. Gérez votre facturation ici : {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -251,7 +251,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||
"go.hero.body":
|
||||
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
|
||||
@@ -263,8 +263,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 il primo mese",
|
||||
"go.pricing.body":
|
||||
"Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: limite d'uso triplicato fino al 27 aprile",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle e modelli gratuiti",
|
||||
"go.graph.go": "Go",
|
||||
@@ -302,7 +300,7 @@ export const dict = {
|
||||
"go.problem.item2": "Limiti generosi e accesso affidabile",
|
||||
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
|
||||
"go.problem.item4":
|
||||
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||
"Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
|
||||
"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.step1.title": "Crea un account",
|
||||
@@ -327,7 +325,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.",
|
||||
"go.faq.q3": "Go è lo stesso di Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Quanto costa Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go costa",
|
||||
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
|
||||
@@ -351,7 +349,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash 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.modelNotSupported": "Modello {{model}} non supportato",
|
||||
@@ -361,8 +359,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Chiave API mancante.",
|
||||
"zen.api.error.invalidApiKey": "Chiave API non valida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota dell'abbonamento superata. Puoi continuare a utilizzare modelli gratuiti.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Limite di utilizzo di 5 ore raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Limite di utilizzo settimanale raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Limite di utilizzo mensile raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Nessun metodo di pagamento. Aggiungi un metodo di pagamento qui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insufficiente. Gestisci la tua fatturazione qui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -250,7 +250,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||
"go.meta.description":
|
||||
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||
"Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||
"go.hero.body":
|
||||
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
|
||||
@@ -262,8 +262,6 @@ export const dict = {
|
||||
"go.cta.promo": "初月 $5",
|
||||
"go.pricing.body":
|
||||
"どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6、4月27日まで利用上限が3倍に",
|
||||
"go.graph.free": "無料",
|
||||
"go.graph.freePill": "Big Pickleと無料モデル",
|
||||
"go.graph.go": "Go",
|
||||
@@ -302,7 +300,7 @@ export const dict = {
|
||||
"go.problem.item2": "十分な制限と安定したアクセス",
|
||||
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
|
||||
"go.problem.item4":
|
||||
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む",
|
||||
"GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む",
|
||||
"go.how.title": "Goの仕組み",
|
||||
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
|
||||
"go.how.step1.title": "アカウントを作成",
|
||||
@@ -327,7 +325,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。",
|
||||
"go.faq.q3": "GoはZenと同じですか?",
|
||||
"go.faq.a3":
|
||||
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
|
||||
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。",
|
||||
"go.faq.q4": "Goの料金は?",
|
||||
"go.faq.a4.p1.beforePricing": "Goは",
|
||||
"go.faq.a4.p1.pricingLink": "最初の月$5",
|
||||
@@ -351,7 +349,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "無料モデルとGoの違いは何ですか?",
|
||||
"go.faq.a9":
|
||||
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
|
||||
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
||||
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
||||
@@ -362,8 +360,12 @@ export const dict = {
|
||||
"zen.api.error.invalidApiKey": "無効なAPIキーです。",
|
||||
"zen.api.error.subscriptionQuotaExceeded":
|
||||
"サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"サブスクリプションの制限を超えました。無料モデルは引き続きご利用いただけます。",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5時間の利用上限に達しました。{{retryIn}} 後にリセットされます。今すぐこのモデルの利用を続けるには、利用可能な残高からの利用を有効化してください: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"週間の利用上限に達しました。{{retryIn}} 後にリセットされます。今すぐこのモデルの利用を続けるには、利用可能な残高からの利用を有効化してください: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"月間の利用上限に達しました。{{retryIn}} 後にリセットされます。今すぐこのモデルの利用を続けるには、利用可能な残高からの利用を有効化してください: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "お支払い方法がありません。こちらからお支払い方法を追加してください: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "残高が不足しています。こちらから請求を管理してください: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -247,7 +247,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||
"go.meta.description":
|
||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||
"go.hero.body":
|
||||
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
|
||||
@@ -259,8 +259,6 @@ export const dict = {
|
||||
"go.cta.promo": "첫 달 $5",
|
||||
"go.pricing.body":
|
||||
"어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6, 4월 27일까지 사용 한도 3배 확대",
|
||||
"go.graph.free": "무료",
|
||||
"go.graph.freePill": "Big Pickle 및 무료 모델",
|
||||
"go.graph.go": "Go",
|
||||
@@ -299,7 +297,7 @@ export const dict = {
|
||||
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
|
||||
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
|
||||
"go.problem.item4":
|
||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함",
|
||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함",
|
||||
"go.how.title": "Go 작동 방식",
|
||||
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
|
||||
"go.how.step1.title": "계정 생성",
|
||||
@@ -323,7 +321,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.",
|
||||
"go.faq.q3": "Go는 Zen과 같은가요?",
|
||||
"go.faq.a3":
|
||||
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
||||
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
||||
"go.faq.q4": "Go 비용은 얼마인가요?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 비용은",
|
||||
"go.faq.a4.p1.pricingLink": "첫 달 $5",
|
||||
@@ -346,7 +344,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
|
||||
"go.faq.a9":
|
||||
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
|
||||
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
||||
@@ -356,8 +354,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "API 키가 누락되었습니다.",
|
||||
"zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"구독 할당량을 초과했습니다. 무료 모델은 계속 사용할 수 있습니다.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5시간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"주간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"월간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "결제 수단이 없습니다. 결제 수단을 추가하세요: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "잔액이 부족합니다. 결제 관리를 여기서 하세요: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -251,7 +251,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||
"go.hero.body":
|
||||
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
|
||||
@@ -263,8 +263,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 første måned",
|
||||
"go.pricing.body":
|
||||
"Bruk med hvilken som helst agent. $5 første måned, deretter $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: bruksgrensen er tredoblet til 27. april",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle og gratis modeller",
|
||||
"go.graph.go": "Go",
|
||||
@@ -302,7 +300,7 @@ export const dict = {
|
||||
"go.problem.item2": "Rause grenser og pålitelig tilgang",
|
||||
"go.problem.item3": "Bygget for så mange programmerere som mulig",
|
||||
"go.problem.item4":
|
||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
|
||||
"go.how.title": "Hvordan Go fungerer",
|
||||
"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.",
|
||||
@@ -328,7 +326,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.",
|
||||
"go.faq.q3": "Er Go det samme som Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Hva koster Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||
"go.faq.a4.p1.pricingLink": "$5 første måned",
|
||||
@@ -352,7 +350,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash 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.modelNotSupported": "Modell {{model}} støttes ikke",
|
||||
@@ -362,8 +360,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Mangler API-nøkkel.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsette å bruke gratis modeller.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5-timers bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Ukentlig bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Månedlig bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Legg til en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrekkelig saldo. Administrer faktureringen din her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -252,7 +252,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.hero.body":
|
||||
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
|
||||
@@ -264,8 +264,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 pierwszy miesiąc",
|
||||
"go.pricing.body":
|
||||
"Używaj z dowolnym agentem. $5 za pierwszy miesiąc, potem $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: limit użycia zwiększony 3× do 27 kwietnia",
|
||||
"go.graph.free": "Darmowe",
|
||||
"go.graph.freePill": "Big Pickle i darmowe modele",
|
||||
"go.graph.go": "Go",
|
||||
@@ -303,7 +301,7 @@ export const dict = {
|
||||
"go.problem.item2": "Hojne limity i niezawodny dostęp",
|
||||
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
|
||||
"go.problem.item4":
|
||||
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash",
|
||||
"Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash",
|
||||
"go.how.title": "Jak działa Go",
|
||||
"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.",
|
||||
@@ -329,7 +327,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.",
|
||||
"go.faq.q3": "Czy Go to to samo co Zen?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||
"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, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Ile kosztuje Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go kosztuje",
|
||||
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
|
||||
@@ -353,7 +351,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash 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.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
||||
@@ -363,8 +361,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Brak klucza API.",
|
||||
"zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Przekroczono limit subskrypcji. Możesz kontynuować korzystanie z darmowych modeli.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Osiągnięto 5-godzinny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Osiągnięto tygodniowy limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Osiągnięto miesięczny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Brak metody płatności. Dodaj metodę płatności tutaj: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Niewystarczające saldo. Zarządzaj swoimi płatnościami tutaj: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -255,7 +255,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||
"go.meta.description":
|
||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||
"go.hero.body":
|
||||
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
|
||||
@@ -267,8 +267,6 @@ export const dict = {
|
||||
"go.cta.promo": "$5 первый месяц",
|
||||
"go.pricing.body":
|
||||
"Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: лимит использования увеличен в 3 раза до 27 апреля",
|
||||
"go.graph.free": "Бесплатно",
|
||||
"go.graph.freePill": "Big Pickle и бесплатные модели",
|
||||
"go.graph.go": "Go",
|
||||
@@ -307,7 +305,7 @@ export const dict = {
|
||||
"go.problem.item2": "Щедрые лимиты и надежный доступ",
|
||||
"go.problem.item3": "Создан для максимального числа программистов",
|
||||
"go.problem.item4":
|
||||
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash",
|
||||
"Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash",
|
||||
"go.how.title": "Как работает Go",
|
||||
"go.how.body":
|
||||
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
|
||||
@@ -333,7 +331,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.",
|
||||
"go.faq.q3": "Go — это то же самое, что и Zen?",
|
||||
"go.faq.a3":
|
||||
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||
"Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||
"go.faq.q4": "Сколько стоит Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go стоит",
|
||||
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
|
||||
@@ -357,7 +355,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
|
||||
"go.faq.a9":
|
||||
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
|
||||
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
||||
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
||||
@@ -367,8 +365,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "Отсутствует API ключ.",
|
||||
"zen.api.error.invalidApiKey": "Неверный API ключ.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Квота подписки превышена. Вы можете продолжить использовать бесплатные модели.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"Достигнут лимит использования за 5 часов. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Достигнут недельный лимит использования. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Достигнут месячный лимит использования. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Нет способа оплаты. Добавьте способ оплаты здесь: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Недостаточно средств. Управляйте оплатой здесь: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -250,7 +250,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.meta.description":
|
||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.hero.body":
|
||||
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
|
||||
@@ -261,8 +261,6 @@ export const dict = {
|
||||
"go.cta.price": "$10/เดือน",
|
||||
"go.cta.promo": "$5 เดือนแรก",
|
||||
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6 โควตาการใช้งานเพิ่มเป็น 3 เท่า ถึง 27 เม.ย.",
|
||||
"go.graph.free": "ฟรี",
|
||||
"go.graph.freePill": "Big Pickle และโมเดลฟรี",
|
||||
"go.graph.go": "Go",
|
||||
@@ -300,7 +298,7 @@ export const dict = {
|
||||
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
|
||||
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
|
||||
"go.problem.item4":
|
||||
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||
"รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||
"go.how.title": "Go ทำงานอย่างไร",
|
||||
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
|
||||
"go.how.step1.title": "สร้างบัญชี",
|
||||
@@ -325,7 +323,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้",
|
||||
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
|
||||
"go.faq.a3":
|
||||
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้",
|
||||
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้",
|
||||
"go.faq.q4": "Go ราคาเท่าไหร่?",
|
||||
"go.faq.a4.p1.beforePricing": "Go ราคา",
|
||||
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
|
||||
@@ -348,7 +346,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
|
||||
"go.faq.a9":
|
||||
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
|
||||
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
||||
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
||||
@@ -358,8 +356,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "ไม่มี API key",
|
||||
"zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"โควต้าการสมัครสมาชิกเกินขีดจำกัด คุณสามารถดำเนินการต่อโดยใช้โมเดลฟรี",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"ถึงขีดจำกัดการใช้งานในรอบ 5 ชั่วโมงแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"ถึงขีดจำกัดการใช้งานรายสัปดาห์แล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"ถึงขีดจำกัดการใช้งานรายเดือนแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "ไม่มีวิธีการชำระเงิน เพิ่มวิธีการชำระเงินที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "ยอดเงินคงเหลือไม่เพียงพอ จัดการการเรียกเก็บเงินของคุณที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.meta.description":
|
||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.hero.body":
|
||||
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
|
||||
@@ -265,8 +265,6 @@ export const dict = {
|
||||
"go.cta.promo": "İlk ay $5",
|
||||
"go.pricing.body":
|
||||
"Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6: kullanım limiti 27 Nisan'a kadar 3 katına çıktı",
|
||||
"go.graph.free": "Ücretsiz",
|
||||
"go.graph.freePill": "Big Pickle ve ücretsiz modeller",
|
||||
"go.graph.go": "Go",
|
||||
@@ -305,7 +303,7 @@ export const dict = {
|
||||
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
|
||||
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
|
||||
"go.problem.item4":
|
||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir",
|
||||
"GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir",
|
||||
"go.how.title": "Go nasıl çalışır?",
|
||||
"go.how.body":
|
||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
|
||||
@@ -331,7 +329,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.",
|
||||
"go.faq.q3": "Go, Zen ile aynı mı?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.",
|
||||
"go.faq.q4": "Go ne kadar?",
|
||||
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
|
||||
"go.faq.a4.p1.pricingLink": "İlk ay $5",
|
||||
@@ -355,7 +353,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
|
||||
"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, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash 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.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash 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.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
||||
@@ -365,8 +363,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "API anahtarı eksik.",
|
||||
"zen.api.error.invalidApiKey": "Geçersiz API anahtarı.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonelik kotası aşıldı. Ücretsiz modelleri kullanmaya devam edebilirsiniz.",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"5 saatlik kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"Haftalık kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"Aylık kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "Ödeme yöntemi bulunamadı. Buradan bir ödeme yöntemi ekleyin: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Yetersiz bakiye. Faturalandırmanızı buradan yönetin: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -241,7 +241,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||
"go.meta.description":
|
||||
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
||||
"Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。",
|
||||
"go.hero.title": "人人可用的低成本编程模型",
|
||||
"go.hero.body":
|
||||
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
|
||||
@@ -252,8 +252,6 @@ export const dict = {
|
||||
"go.cta.price": "$10/月",
|
||||
"go.cta.promo": "首月 $5",
|
||||
"go.pricing.body": "可配合任何代理使用。首月 $5,之后 $10/月。如有需要可充值。随时取消。",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6 使用额度提升至 3 倍,限时至 4 月 27 日",
|
||||
"go.graph.free": "免费",
|
||||
"go.graph.freePill": "Big Pickle 和免费模型",
|
||||
"go.graph.go": "Go",
|
||||
@@ -291,7 +289,7 @@ export const dict = {
|
||||
"go.problem.item2": "充裕的限额和可靠的访问",
|
||||
"go.problem.item3": "为尽可能多的程序员打造",
|
||||
"go.problem.item4":
|
||||
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash",
|
||||
"包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash",
|
||||
"go.how.title": "Go 如何工作",
|
||||
"go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
|
||||
"go.how.step1.title": "创建账户",
|
||||
@@ -313,7 +311,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。",
|
||||
"go.faq.q3": "Go 和 Zen 一样吗?",
|
||||
"go.faq.a3":
|
||||
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。",
|
||||
"不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。",
|
||||
"go.faq.q4": "Go 多少钱?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 费用为",
|
||||
"go.faq.a4.p1.pricingLink": "首月 $5",
|
||||
@@ -335,7 +333,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
|
||||
"go.faq.a9":
|
||||
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
|
||||
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
||||
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
||||
@@ -345,7 +343,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "缺少 API 密钥。",
|
||||
"zen.api.error.invalidApiKey": "无效的 API 密钥。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出订阅配额。您可以继续使用免费模型。",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"已达到 5 小时使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"已达到每周使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"已达到每月使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "没有付款方式。请在此处添加付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "余额不足。请在此处管理您的计费:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -241,7 +241,7 @@ export const dict = {
|
||||
|
||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||
"go.meta.description":
|
||||
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
||||
"Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。",
|
||||
"go.hero.title": "低成本全民編碼模型",
|
||||
"go.hero.body":
|
||||
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
|
||||
@@ -252,8 +252,6 @@ export const dict = {
|
||||
"go.cta.price": "$10/月",
|
||||
"go.cta.promo": "首月 $5",
|
||||
"go.pricing.body": "可搭配任何代理使用。首月 $5,之後 $10/月。如有需要可儲值。隨時取消。",
|
||||
"go.banner.badge": "3x",
|
||||
"go.banner.text": "Kimi K2.6 使用額度提升至 3 倍,限時至 4 月 27 日",
|
||||
"go.graph.free": "免費",
|
||||
"go.graph.freePill": "Big Pickle 與免費模型",
|
||||
"go.graph.go": "Go",
|
||||
@@ -291,7 +289,7 @@ export const dict = {
|
||||
"go.problem.item2": "寬裕的限額與穩定存取",
|
||||
"go.problem.item3": "專為盡可能多的程式設計師打造",
|
||||
"go.problem.item4":
|
||||
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash",
|
||||
"包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash",
|
||||
"go.how.title": "Go 如何運作",
|
||||
"go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
|
||||
"go.how.step1.title": "建立帳號",
|
||||
@@ -313,7 +311,7 @@ export const dict = {
|
||||
"go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。",
|
||||
"go.faq.q3": "Go 與 Zen 一樣嗎?",
|
||||
"go.faq.a3":
|
||||
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。",
|
||||
"不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。",
|
||||
"go.faq.q4": "Go 費用是多少?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 費用為",
|
||||
"go.faq.a4.p1.pricingLink": "首月 $5",
|
||||
@@ -335,7 +333,7 @@ export const dict = {
|
||||
|
||||
"go.faq.q9": "免費模型與 Go 有什麼區別?",
|
||||
"go.faq.a9":
|
||||
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
|
||||
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
||||
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
||||
@@ -345,7 +343,12 @@ export const dict = {
|
||||
"zen.api.error.missingApiKey": "缺少 API 金鑰。",
|
||||
"zen.api.error.invalidApiKey": "無效的 API 金鑰。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出訂閱配額。你可以繼續使用免費模型。",
|
||||
"zen.api.error.goSubscriptionRollingLimitExceeded":
|
||||
"已達 5 小時使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
|
||||
"已達每週使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}",
|
||||
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
|
||||
"已達每月使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}",
|
||||
"zen.api.error.noPaymentMethod": "無付款方式。請在此處新增付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "餘額不足。請在此處管理你的帳務:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { json } from "@solidjs/router"
|
||||
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
|
||||
export async function GET(_evt: APIEvent) {
|
||||
return json({
|
||||
data: await Database.use(async (tx) => {
|
||||
const result = await tx.$count(UserTable)
|
||||
return result
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -326,37 +326,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="desktop-app-banner"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
[data-slot="badge"] {
|
||||
background: var(--color-background-strong);
|
||||
color: var(--color-text-inverted);
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
[data-slot="text"] {
|
||||
color: var(--color-text-strong);
|
||||
line-height: 1.4;
|
||||
|
||||
@media (max-width: 30.625rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="hero-copy"] {
|
||||
img {
|
||||
margin-bottom: 24px;
|
||||
@@ -662,10 +631,6 @@ body {
|
||||
fill: var(--color-text-strong);
|
||||
}
|
||||
|
||||
[data-bar][data-kind="promo"] {
|
||||
fill: color-mix(in srgb, var(--bar-go) 50%, transparent);
|
||||
}
|
||||
|
||||
[data-val] {
|
||||
fill: var(--color-text-strong);
|
||||
font-size: 13px;
|
||||
|
||||
@@ -27,8 +27,6 @@ const models = [
|
||||
{ name: "GLM-5", provider: "DeepInfra, Fireworks AI, Z.ai" },
|
||||
{ name: "Kimi K2.5", provider: "Moonshot AI" },
|
||||
{ name: "Kimi K2.6", provider: "Moonshot AI" },
|
||||
{ name: "MiMo-V2-Pro", provider: "Xiaomi MiMo" },
|
||||
{ name: "MiMo-V2-Omni", provider: "Xiaomi MiMo" },
|
||||
{ name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" },
|
||||
{ name: "MiMo-V2.5", provider: "Xiaomi MiMo" },
|
||||
{ name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" },
|
||||
@@ -63,7 +61,7 @@ function LimitsGraph(props: { href: string }) {
|
||||
const free = 200
|
||||
const graph = [
|
||||
{ id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" },
|
||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" },
|
||||
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" },
|
||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" },
|
||||
@@ -157,24 +155,12 @@ function LimitsGraph(props: { href: string }) {
|
||||
<rect
|
||||
x={left}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, x(ratio(m.baseReq ?? m.req)) - left)}
|
||||
width={Math.max(0, x(ratio(m.req)) - left)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
data-segment={m.baseReq ? "base" : undefined}
|
||||
/>
|
||||
{m.baseReq && (
|
||||
<rect
|
||||
x={x(ratio(m.baseReq)) + 2}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, x(ratio(m.req)) - x(ratio(m.baseReq)) - 2)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind="promo"
|
||||
data-model={m.id}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
</For>
|
||||
@@ -264,12 +250,6 @@ export default function Home() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="hero">
|
||||
<div data-component="desktop-app-banner">
|
||||
<span data-slot="badge">{i18n.t("home.banner.badge")}</span>
|
||||
<div data-slot="content">
|
||||
<span data-slot="text">{i18n.t("go.banner.text")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="hero-copy">
|
||||
<img data-slot="zen logo light" src={goLogoLight} alt="" />
|
||||
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { z } from "zod"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
|
||||
const DISCORD_ALERT_ROLE_ID = "1501447160175136838"
|
||||
|
||||
const basePayload = z.object({
|
||||
name: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
isTest: z.boolean().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
|
||||
const groups = z
|
||||
.object({
|
||||
result: z.union([z.number(), z.string()]).nullish(),
|
||||
group: z.object({ key: z.string(), value: z.string() }).array(),
|
||||
})
|
||||
.array()
|
||||
|
||||
const honeycombWebhookPayload = z.discriminatedUnion("type", [
|
||||
basePayload.extend({
|
||||
type: z.literal("model_http_errors"),
|
||||
groups,
|
||||
}),
|
||||
basePayload.extend({
|
||||
type: z.literal("model_low_tps"),
|
||||
groups,
|
||||
}),
|
||||
basePayload.extend({
|
||||
type: z.literal("provider_http_errors"),
|
||||
groups,
|
||||
}),
|
||||
basePayload.extend({
|
||||
type: z.literal("custom"),
|
||||
}),
|
||||
])
|
||||
|
||||
const postDiscordMessage = async (payload: z.infer<typeof honeycombWebhookPayload>) => {
|
||||
const names =
|
||||
payload.type === "custom"
|
||||
? []
|
||||
: payload.groups.flatMap((item) =>
|
||||
item.group.map((g) => {
|
||||
const result = item.result == null ? undefined : Number(item.result)
|
||||
return `- ${g.value}${
|
||||
result !== undefined && Number.isFinite(result)
|
||||
? payload.type === "model_low_tps"
|
||||
? ` (${Math.round(result)} TPS)`
|
||||
: ` (${Math.round(result * 100)}% errors)`
|
||||
: ""
|
||||
}`
|
||||
}),
|
||||
)
|
||||
|
||||
const content = [
|
||||
`[**${payload.isTest ? "[TEST] " : ""}${payload.name ?? "Honeycomb alert"}**](${payload.url})`,
|
||||
...names,
|
||||
"",
|
||||
`<@&${DISCORD_ALERT_ROLE_ID}>`,
|
||||
]
|
||||
.filter((line) => line !== undefined)
|
||||
.join("\n")
|
||||
|
||||
return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
allowed_mentions: { roles: [DISCORD_ALERT_ROLE_ID] },
|
||||
flags: 4,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(input: APIEvent) {
|
||||
const token = input.request.headers.get("X-Honeycomb-Webhook-Token")
|
||||
if (!safeEqual(token ?? "", Resource.HoneycombWebhookSecret.value)) {
|
||||
console.debug("Invalid Honeycomb webhook token")
|
||||
return Response.json({ message: "invalid token" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await input.request.json()
|
||||
console.log(body, JSON.stringify(body, null, 2))
|
||||
|
||||
const parsed = honeycombWebhookPayload.safeParse(body)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error(parsed.error)
|
||||
return Response.json({ message: "invalid payload" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (parsed.data.status !== "TRIGGERED") {
|
||||
console.debug("Skipping resolved alert Honeycomb webhook")
|
||||
return Response.json({ message: "ignored" }, { status: 200 })
|
||||
}
|
||||
|
||||
const response = await postDiscordMessage(parsed.data)
|
||||
if (!response.ok) {
|
||||
return Response.json({ message: "discord webhook failed" }, { status: 502 })
|
||||
}
|
||||
|
||||
return Response.json({ message: "sent" }, { status: 200 })
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Webhook } from "svix"
|
||||
|
||||
type Incident = {
|
||||
mode?: "test" | "standard"
|
||||
name?: string
|
||||
permalink?: string
|
||||
summary?: string
|
||||
}
|
||||
|
||||
type IncidentWebhookPayload = {
|
||||
event_type?: string
|
||||
"public_incident.incident_created_v2"?: Incident
|
||||
}
|
||||
|
||||
const verifyWebhook = async (request: Request) => {
|
||||
const body = await request.text()
|
||||
try {
|
||||
return new Webhook(Resource.INCIDENT_WEBHOOK_SIGNING_SECRET.value).verify(
|
||||
body,
|
||||
Object.fromEntries(request.headers.entries()),
|
||||
) as IncidentWebhookPayload
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const postDiscordMessage = async (incident: Incident) => {
|
||||
return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: [
|
||||
`**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`,
|
||||
incident.summary,
|
||||
"",
|
||||
"@everyone",
|
||||
"",
|
||||
incident.permalink,
|
||||
]
|
||||
.filter((line) => line !== undefined)
|
||||
.join("\n"),
|
||||
allowed_mentions: {
|
||||
parse: ["everyone"],
|
||||
},
|
||||
flags: 4,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(input: APIEvent) {
|
||||
const payload = await verifyWebhook(input.request)
|
||||
if (!payload) {
|
||||
return Response.json({ message: "invalid signature" }, { status: 401 })
|
||||
}
|
||||
|
||||
if (payload.event_type !== "public_incident.incident_created_v2") {
|
||||
return Response.json({ message: "ignored event" }, { status: 200 })
|
||||
}
|
||||
|
||||
const incident = payload["public_incident.incident_created_v2"]
|
||||
if (!incident) {
|
||||
return Response.json({ message: "missing incident" }, { status: 400 })
|
||||
}
|
||||
|
||||
const response = await postDiscordMessage(incident)
|
||||
if (!response.ok) {
|
||||
return Response.json({ message: "discord webhook failed" }, { status: 502 })
|
||||
}
|
||||
|
||||
return Response.json({ message: "sent" }, { status: 200 })
|
||||
}
|
||||
@@ -161,7 +161,9 @@ export async function POST(input: APIEvent) {
|
||||
})
|
||||
|
||||
if (userEmail) {
|
||||
if (coupon === LiteData.firstMonth100Coupon) {
|
||||
if (coupon === LiteData.firstMonth50Coupon) {
|
||||
await Billing.redeemCoupon(userEmail, "GO1MONTH50")
|
||||
} else if (coupon === LiteData.firstMonth100Coupon) {
|
||||
await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
|
||||
} else if (coupon === LiteData.threeMonths100Coupon) {
|
||||
await Billing.redeemCoupon(userEmail, "GO3MONTHS100")
|
||||
|
||||
@@ -289,8 +289,6 @@ export function LiteSection() {
|
||||
<li>Kimi K2.6</li>
|
||||
<li>GLM-5</li>
|
||||
<li>GLM-5.1</li>
|
||||
<li>MiMo-V2-Pro</li>
|
||||
<li>MiMo-V2-Omni</li>
|
||||
<li>MiMo-V2.5-Pro</li>
|
||||
<li>MiMo-V2.5</li>
|
||||
<li>MiniMax M2.5</li>
|
||||
|
||||
@@ -45,6 +45,7 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
all: Object.entries(ZenData.list("full").models)
|
||||
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
||||
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||
.filter(([id, _model]) => !id.endsWith(":global"))
|
||||
.sort(([idA, modelA], [idB, modelB]) => {
|
||||
const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"]
|
||||
const getPriority = (id: string) => {
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -79,6 +80,7 @@
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function UsageSection() {
|
||||
}
|
||||
|
||||
const calculateTotalOutputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
||||
return u.outputTokens + (u.reasoningTokens ?? 0)
|
||||
return u.outputTokens
|
||||
}
|
||||
|
||||
const goPrev = async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { handler } from "~/routes/zen/util/handler"
|
||||
import { parseOpenAiVariant } from "~/routes/zen/util/variant"
|
||||
|
||||
export function POST(input: APIEvent) {
|
||||
return handler(input, {
|
||||
@@ -7,6 +8,7 @@ export function POST(input: APIEvent) {
|
||||
modelList: "lite",
|
||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||
parseModel: (url: string, body: any) => body.model,
|
||||
parseVariant: (url: string, body: any) => parseOpenAiVariant(body),
|
||||
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { handler } from "~/routes/zen/util/handler"
|
||||
import { parseAnthropicVariant } from "~/routes/zen/util/variant"
|
||||
|
||||
export function POST(input: APIEvent) {
|
||||
return handler(input, {
|
||||
@@ -7,6 +8,7 @@ export function POST(input: APIEvent) {
|
||||
modelList: "lite",
|
||||
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
|
||||
parseModel: (url: string, body: any) => body.model,
|
||||
parseVariant: (url: string, body: any) => parseAnthropicVariant(body),
|
||||
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource, waitUntil } from "@opencode-ai/console-resource"
|
||||
|
||||
export function createDataDumper(sessionId: string, requestId: string, projectId: string) {
|
||||
return
|
||||
if (Resource.App.stage !== "production") return
|
||||
if (sessionId === "") return
|
||||
|
||||
|
||||
@@ -13,4 +13,15 @@ class LimitError extends Error {
|
||||
}
|
||||
export class RateLimitError extends LimitError {}
|
||||
export class FreeUsageLimitError extends LimitError {}
|
||||
export class SubscriptionUsageLimitError extends LimitError {}
|
||||
export class BlackUsageLimitError extends LimitError {}
|
||||
|
||||
type LimitName = "5 hour" | "weekly" | "monthly"
|
||||
export class GoUsageLimitError extends LimitError {
|
||||
workspace: string
|
||||
limitName: LimitName
|
||||
constructor(message: string, workspace: string, limitName: LimitName, retryAfter?: number) {
|
||||
super(message, retryAfter)
|
||||
this.workspace = workspace
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
ModelError,
|
||||
RateLimitError,
|
||||
FreeUsageLimitError,
|
||||
SubscriptionUsageLimitError,
|
||||
GoUsageLimitError,
|
||||
BlackUsageLimitError,
|
||||
} from "./error"
|
||||
import {
|
||||
buildCostChunk,
|
||||
@@ -46,6 +47,7 @@ import { Resource } from "@opencode-ai/console-resource"
|
||||
import { i18n, type Key } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
import { createModelTpmLimiter } from "./modelTpmLimiter"
|
||||
import { createModelTpsLimiter } from "./modelTpsLimiter"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -70,6 +72,7 @@ export async function handler(
|
||||
modelList: "lite" | "full"
|
||||
parseApiKey: (headers: Headers) => string | undefined
|
||||
parseModel: (url: string, body: any) => string
|
||||
parseVariant: (url: string, body: any) => string | undefined
|
||||
parseIsStream: (url: string, body: any) => boolean
|
||||
},
|
||||
) {
|
||||
@@ -92,6 +95,7 @@ export async function handler(
|
||||
const url = input.request.url
|
||||
const body = await input.request.json()
|
||||
const model = opts.parseModel(url, body)
|
||||
const variant = opts.parseVariant(url, body)
|
||||
const isStream = opts.parseIsStream(url, body)
|
||||
const rawIp = input.request.headers.get("x-real-ip") ?? ""
|
||||
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
|
||||
@@ -108,6 +112,7 @@ export async function handler(
|
||||
request: requestId,
|
||||
client: ocClient,
|
||||
user_agent: userAgent,
|
||||
"model.variant": variant,
|
||||
})
|
||||
const zenData = ZenData.list(opts.modelList)
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
@@ -116,15 +121,17 @@ export async function handler(
|
||||
const trialProviders = await trialLimiter?.check()
|
||||
const rateLimiter = modelInfo.allowAnonymous
|
||||
? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)
|
||||
: createKeyRateLimiter(modelInfo.id, zenApiKey, input.request)
|
||||
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
const authInfo = await authenticate(modelInfo, zenApiKey)
|
||||
const billingSource = validateBilling(authInfo, modelInfo)
|
||||
logger.metric({ source: billingSource })
|
||||
const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)
|
||||
const modelTpmLimits = await modelTpmLimiter?.check()
|
||||
const modelTpsLimiter = createModelTpsLimiter(modelInfo.providers)
|
||||
const modelTpsLimits = await modelTpsLimiter?.check()
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(
|
||||
@@ -138,6 +145,7 @@ export async function handler(
|
||||
retry,
|
||||
stickyProvider,
|
||||
modelTpmLimits,
|
||||
modelTpsLimits,
|
||||
)
|
||||
validateModelSettings(billingSource, authInfo)
|
||||
updateProviderKey(authInfo, providerInfo)
|
||||
@@ -161,6 +169,7 @@ export async function handler(
|
||||
if (typeof v === "string") {
|
||||
if (v === "$ip") return [[k, ip]]
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||
if (v === "$session") return sessionId ? [[k, sessionId]] : []
|
||||
if (v.startsWith("$header.")) {
|
||||
const headerValue = input.request.headers.get(v.slice(8))
|
||||
return headerValue ? [[k, headerValue]] : []
|
||||
@@ -207,7 +216,7 @@ export async function handler(
|
||||
// ie. 400 error is usually provider error like malformed request
|
||||
res.status !== 400 &&
|
||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||
res.status !== 404 &&
|
||||
!(modelInfo.id.startsWith("gpt-") && res.status === 404) &&
|
||||
// ie. cannot change codex model providers mid-session
|
||||
modelInfo.stickyProvider !== "strict" &&
|
||||
modelInfo.fallbackProvider &&
|
||||
@@ -229,7 +238,7 @@ export async function handler(
|
||||
dataDumper?.provideRequest(reqBody)
|
||||
|
||||
// Store sticky provider
|
||||
await stickyTracker?.set(providerInfo.id)
|
||||
if (res.status === 200) await stickyTracker?.set(providerInfo.id)
|
||||
|
||||
// Temporarily change 404 to 400 status code b/c solid start automatically override 404 response
|
||||
const resStatus = res.status === 404 ? 400 : res.status
|
||||
@@ -289,14 +298,16 @@ export async function handler(
|
||||
|
||||
let buffer = ""
|
||||
let responseLength = 0
|
||||
let timestampFirstByte = 0
|
||||
|
||||
function pump(): Promise<void> {
|
||||
return (
|
||||
reader?.read().then(async ({ done, value: rawValue }) => {
|
||||
if (done) {
|
||||
const timestampLastByte = Date.now()
|
||||
logger.metric({
|
||||
response_length: responseLength,
|
||||
"timestamp.last_byte": Date.now(),
|
||||
"timestamp.last_byte": timestampLastByte,
|
||||
})
|
||||
dataDumper?.flush()
|
||||
await rateLimiter?.track()
|
||||
@@ -306,6 +317,14 @@ export async function handler(
|
||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||
await trialLimiter?.track(usageInfo)
|
||||
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||
await modelTpsLimiter?.track(
|
||||
providerInfo.id,
|
||||
providerInfo.model,
|
||||
providerInfo.tpsGoal,
|
||||
timestampFirstByte,
|
||||
timestampLastByte,
|
||||
usageInfo,
|
||||
)
|
||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||
await reload(billingSource, authInfo, costInfo)
|
||||
const cost = calculateOccurredCost(billingSource, costInfo)
|
||||
@@ -316,10 +335,10 @@ export async function handler(
|
||||
}
|
||||
|
||||
if (responseLength === 0) {
|
||||
const now = Date.now()
|
||||
timestampFirstByte = Date.now()
|
||||
logger.metric({
|
||||
time_to_first_byte: now - startTimestamp,
|
||||
"timestamp.first_byte": now,
|
||||
time_to_first_byte: timestampFirstByte - startTimestamp,
|
||||
"timestamp.first_byte": timestampFirstByte,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -395,7 +414,8 @@ export async function handler(
|
||||
if (
|
||||
error instanceof RateLimitError ||
|
||||
error instanceof FreeUsageLimitError ||
|
||||
error instanceof SubscriptionUsageLimitError
|
||||
error instanceof GoUsageLimitError ||
|
||||
error instanceof BlackUsageLimitError
|
||||
) {
|
||||
const headers = new Headers()
|
||||
if (error.retryAfter) {
|
||||
@@ -404,7 +424,17 @@ export async function handler(
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error: { type: error.constructor.name, message: error.message },
|
||||
error: {
|
||||
type: error.constructor.name,
|
||||
message: error.message,
|
||||
},
|
||||
metadata:
|
||||
error instanceof GoUsageLimitError
|
||||
? {
|
||||
workspace: error.workspace,
|
||||
limitName: error.limitName,
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
{ status: 429, headers },
|
||||
)
|
||||
@@ -462,6 +492,7 @@ export async function handler(
|
||||
retry: RetryOptions,
|
||||
stickyProvider: string | undefined,
|
||||
modelTpmLimits: Record<string, number> | undefined,
|
||||
modelTpsLimits: Record<string, boolean> | undefined,
|
||||
) {
|
||||
const modelProvider = (() => {
|
||||
// Byok is top priority b/c if user set their own API key, we should use it
|
||||
@@ -493,6 +524,11 @@ export async function handler(
|
||||
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
|
||||
return usage < provider.tpmLimit * 1_000_000
|
||||
})
|
||||
.filter((provider) => {
|
||||
if (!provider.tpsGoal) return true
|
||||
const isLowTps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? false
|
||||
return !isLowTps
|
||||
})
|
||||
.map((provider) => {
|
||||
topPriority = Math.min(topPriority, provider.priority)
|
||||
return provider
|
||||
@@ -693,7 +729,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeFixedUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
throw new BlackUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
@@ -711,7 +747,7 @@ export async function handler(
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
throw new BlackUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
@@ -728,6 +764,7 @@ export async function handler(
|
||||
// Validate lite subscription billing
|
||||
if (opts.modelList === "lite" && authInfo.billing.lite && authInfo.lite) {
|
||||
try {
|
||||
const consoleGoUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/go`
|
||||
const sub = authInfo.lite
|
||||
const liteData = LiteData.getLimits()
|
||||
|
||||
@@ -739,8 +776,13 @@ export async function handler(
|
||||
timeUpdated: sub.timeWeeklyUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
throw new GoUsageLimitError(
|
||||
t("zen.api.error.goSubscriptionWeeklyLimitExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
consoleGoUrl,
|
||||
}),
|
||||
authInfo.workspaceID,
|
||||
"weekly",
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -754,8 +796,13 @@ export async function handler(
|
||||
timeSubscribed: sub.timeCreated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
throw new GoUsageLimitError(
|
||||
t("zen.api.error.goSubscriptionMonthlyLimitExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
consoleGoUrl,
|
||||
}),
|
||||
authInfo.workspaceID,
|
||||
"monthly",
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -769,8 +816,13 @@ export async function handler(
|
||||
timeUpdated: sub.timeRollingUpdated,
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
throw new GoUsageLimitError(
|
||||
t("zen.api.error.goSubscriptionRollingLimitExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
consoleGoUrl,
|
||||
}),
|
||||
authInfo.workspaceID,
|
||||
"5 hour",
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -857,10 +909,6 @@ export async function handler(
|
||||
|
||||
const inputCost = modelCost.input * inputTokens * 100
|
||||
const outputCost = modelCost.output * outputTokens * 100
|
||||
const reasoningCost = (() => {
|
||||
if (!reasoningTokens) return undefined
|
||||
return modelCost.output * reasoningTokens * 100
|
||||
})()
|
||||
const cacheReadCost = (() => {
|
||||
if (!cacheReadTokens) return undefined
|
||||
if (!modelCost.cacheRead) return undefined
|
||||
@@ -877,17 +925,11 @@ export async function handler(
|
||||
return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
|
||||
})()
|
||||
const totalCostInCent =
|
||||
inputCost +
|
||||
outputCost +
|
||||
(reasoningCost ?? 0) +
|
||||
(cacheReadCost ?? 0) +
|
||||
(cacheWrite5mCost ?? 0) +
|
||||
(cacheWrite1hCost ?? 0)
|
||||
inputCost + outputCost + (cacheReadCost ?? 0) + (cacheWrite5mCost ?? 0) + (cacheWrite1hCost ?? 0)
|
||||
return {
|
||||
totalCostInCent,
|
||||
inputCost,
|
||||
outputCost,
|
||||
reasoningCost,
|
||||
cacheReadCost,
|
||||
cacheWrite5mCost,
|
||||
cacheWrite1hCost,
|
||||
@@ -909,8 +951,7 @@ export async function handler(
|
||||
) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
usageInfo
|
||||
const { totalCostInCent, inputCost, outputCost, reasoningCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } =
|
||||
costInfo
|
||||
const { totalCostInCent, inputCost, outputCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } = costInfo
|
||||
|
||||
logger.metric({
|
||||
"tokens.input": inputTokens,
|
||||
@@ -921,14 +962,12 @@ export async function handler(
|
||||
"tokens.cache_write_1h": cacheWrite1hTokens,
|
||||
"cost.input.microcents": centsToMicroCents(inputCost),
|
||||
"cost.output.microcents": centsToMicroCents(outputCost),
|
||||
"cost.reasoning.microcents": reasoningCost ? centsToMicroCents(reasoningCost) : undefined,
|
||||
"cost.cache_read.microcents": cacheReadCost ? centsToMicroCents(cacheReadCost) : undefined,
|
||||
"cost.cache_write.microcents": cacheWrite5mCost ? centsToMicroCents(cacheWrite5mCost) : undefined,
|
||||
"cost.total.microcents": centsToMicroCents(totalCostInCent),
|
||||
// deprecated - remove after May 20, 2026
|
||||
"cost.input": Math.round(inputCost),
|
||||
"cost.output": Math.round(outputCost),
|
||||
"cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
|
||||
"cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
|
||||
"cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
|
||||
"cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
|
||||
|
||||
@@ -10,8 +10,11 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limits = Subscription.getFreeLimits()
|
||||
const dailyLimit = rateLimit ?? limits.dailyRequests
|
||||
const isDefaultModel = !rateLimit
|
||||
const headersExist = Object.entries(limits.checkHeaders).every(
|
||||
([name, value]) => request.headers.get(name)?.toLowerCase().includes(value) ?? false,
|
||||
)
|
||||
const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
||||
const isDefaultModel = headersExist && !rateLimit
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
|
||||
@@ -4,11 +4,16 @@ import { RateLimitError } from "./error"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
export function createRateLimiter(modelId: string, zenApiKey: string | undefined, request: Request) {
|
||||
export function createRateLimiter(
|
||||
modelId: string,
|
||||
rateLimit: number | undefined,
|
||||
zenApiKey: string | undefined,
|
||||
request: Request,
|
||||
) {
|
||||
if (!zenApiKey) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const LIMIT = 100
|
||||
const LIMIT = rateLimit ?? 500
|
||||
const yyyyMMddHHmm = new Date(Date.now())
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { and, Database, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { ModelTpsRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
|
||||
export function createModelTpsLimiter(providers: { id: string; model: string; tpsGoal?: number }[]) {
|
||||
const tpsGoals = Object.fromEntries(
|
||||
providers.flatMap((p) => {
|
||||
return p.tpsGoal ? [[`${p.id}/${p.model}/${p.tpsGoal}`, p.tpsGoal]] : []
|
||||
}),
|
||||
)
|
||||
const ids = Object.keys(tpsGoals)
|
||||
if (ids.length === 0) return
|
||||
|
||||
const toInterval = (date: Date) =>
|
||||
parseInt(
|
||||
date
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
.substring(0, 12),
|
||||
)
|
||||
const now = Date.now()
|
||||
const currInterval = toInterval(new Date(now))
|
||||
const prevInterval = toInterval(new Date(now - 60 * 1000))
|
||||
|
||||
return {
|
||||
check: async () => {
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(ModelTpsRateLimitTable)
|
||||
.where(
|
||||
and(
|
||||
inArray(ModelTpsRateLimitTable.id, ids),
|
||||
inArray(ModelTpsRateLimitTable.interval, [currInterval, prevInterval]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// convert to map of model to summed count across current and previous intervals
|
||||
const result = data.reduce(
|
||||
(acc, curr) => {
|
||||
const existing = acc[curr.id] ?? { qualify: 0, unqualify: 0 }
|
||||
acc[curr.id] = {
|
||||
qualify: existing.qualify + curr.qualify,
|
||||
unqualify: existing.unqualify + curr.unqualify,
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, { qualify: number; unqualify: number }>,
|
||||
)
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(result).map(([id, { qualify, unqualify }]) => {
|
||||
const isLowTps = qualify + unqualify > 10 && qualify < unqualify
|
||||
return [id, isLowTps]
|
||||
}),
|
||||
)
|
||||
},
|
||||
track: async (
|
||||
provider: string,
|
||||
model: string,
|
||||
tpsGoal: number | undefined,
|
||||
tsFirstByte: number,
|
||||
tsLastByte: number,
|
||||
usageInfo: UsageInfo,
|
||||
) => {
|
||||
if (!tpsGoal) return
|
||||
const id = `${provider}/${model}/${tpsGoal}`
|
||||
if (!ids.includes(id)) return
|
||||
if (tsFirstByte <= 0 || tsLastByte <= 0) return
|
||||
const tokens = usageInfo.outputTokens
|
||||
if (tokens <= 10) return
|
||||
|
||||
const tps = (tokens / (tsLastByte - tsFirstByte)) * 1000
|
||||
const qualify = tps >= tpsGoal ? 1 : 0
|
||||
const unqualify = tps < tpsGoal ? 1 : 0
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(ModelTpsRateLimitTable)
|
||||
.values({
|
||||
id,
|
||||
interval: currInterval,
|
||||
qualify,
|
||||
unqualify,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
qualify: sql`${ModelTpsRateLimitTable.qualify} + ${qualify}`,
|
||||
unqualify: sql`${ModelTpsRateLimitTable.unqualify} + ${unqualify}`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
|
||||
const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
|
||||
return {
|
||||
inputTokens: inputTokens - (cacheReadTokens ?? 0),
|
||||
outputTokens: outputTokens - (reasoningTokens ?? 0),
|
||||
outputTokens,
|
||||
reasoningTokens,
|
||||
cacheReadTokens,
|
||||
cacheWrite5mTokens: undefined,
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { ModelStickyProviderTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
|
||||
export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
|
||||
export function createStickyTracker(modelId: string, stickyProvider: "strict" | "prefer" | undefined, session: string) {
|
||||
if (!stickyProvider) return
|
||||
if (!session) return
|
||||
const key = `sticky:${session}`
|
||||
const id = `${modelId}/${session}`
|
||||
let _providerId: string | undefined
|
||||
|
||||
return {
|
||||
get: async () => {
|
||||
return await Resource.GatewayKv.get(key)
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
providerId: ModelStickyProviderTable.providerId,
|
||||
})
|
||||
.from(ModelStickyProviderTable)
|
||||
.where(eq(ModelStickyProviderTable.id, id))
|
||||
.limit(1),
|
||||
)
|
||||
_providerId = data[0]?.providerId
|
||||
return _providerId
|
||||
},
|
||||
set: async (providerId: string) => {
|
||||
await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
|
||||
if (_providerId === providerId) return
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(ModelStickyProviderTable)
|
||||
.values({
|
||||
id,
|
||||
providerId,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
providerId,
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export function parseAnthropicVariant(body: any) {
|
||||
const effort = body.effort ?? body.output_config?.effort ?? body.outputConfig?.effort ?? body.thinking?.effort
|
||||
if (effort) return effort
|
||||
|
||||
const budget = body.thinking?.budget_tokens ?? body.thinking?.budgetTokens
|
||||
if (body.thinking?.type !== "enabled" || typeof budget !== "number") return undefined
|
||||
return budget > 16_000 ? "max" : "high"
|
||||
}
|
||||
|
||||
export function parseGoogleVariant(body: any) {
|
||||
const thinkingConfig = body.generationConfig?.thinkingConfig ?? body.thinkingConfig
|
||||
if (thinkingConfig?.thinkingLevel) return thinkingConfig.thinkingLevel
|
||||
|
||||
const budget = thinkingConfig?.thinkingBudget ?? thinkingConfig?.thinking_budget
|
||||
if (typeof budget !== "number" || budget <= 0) return undefined
|
||||
return budget > 16_000 ? "max" : "high"
|
||||
}
|
||||
|
||||
export function parseOpenAiVariant(body: any) {
|
||||
return body.reasoningEffort ?? body.reasoning_effort ?? body.reasoning?.effort
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { handler } from "~/routes/zen/util/handler"
|
||||
import { parseOpenAiVariant } from "~/routes/zen/util/variant"
|
||||
|
||||
export function POST(input: APIEvent) {
|
||||
return handler(input, {
|
||||
@@ -7,6 +8,7 @@ export function POST(input: APIEvent) {
|
||||
modelList: "full",
|
||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||
parseModel: (url: string, body: any) => body.model,
|
||||
parseVariant: (url: string, body: any) => parseOpenAiVariant(body),
|
||||
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user