mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 09:39:46 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa56a8a71 | |||
| b76df6c359 | |||
| e3366180ae |
@@ -322,7 +322,6 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -449,7 +449,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +509,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +559,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -690,7 +690,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +766,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +796,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -836,7 +836,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -849,7 +849,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -883,7 +883,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -902,7 +902,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -944,7 +944,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -971,7 +971,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1022,7 +1022,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1078,7 +1078,6 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
|
||||
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
|
||||
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
|
||||
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
|
||||
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=",
|
||||
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=",
|
||||
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=",
|
||||
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -158,7 +158,6 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch"
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
||||
- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts.
|
||||
- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code.
|
||||
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
@@ -15,7 +14,6 @@ export type DialogGoUpsellProps = {
|
||||
|
||||
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
const runAction = () => {
|
||||
@@ -34,7 +32,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
<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}>
|
||||
{language.t("dialog.usageExceeded.dontShowAgain")}
|
||||
Don't show again
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={runAction}>
|
||||
{props.actionLabel}
|
||||
|
||||
@@ -5,15 +5,12 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { File } from "@opencode-ai/session-ui/file"
|
||||
import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -223,31 +220,6 @@ export function SessionContextTab() {
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
] satisfies { label: string; value: () => JSX.Element }[]
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
@@ -356,18 +328,7 @@ export function SessionContextTab() {
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="gap-1.5 px-2 text-text-weak hover:text-text-base"
|
||||
onClick={exportSession}
|
||||
>
|
||||
<Icon name="download" size="small" />
|
||||
<span>{language.t("context.export.session")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Accordion multiple>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
|
||||
@@ -628,7 +628,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||
aria-label={props.state.ariaLabel}
|
||||
>
|
||||
<span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0">
|
||||
{props.state.label}
|
||||
Update
|
||||
</span>
|
||||
<span class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Show
|
||||
|
||||
@@ -9,7 +9,6 @@ import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import {
|
||||
createDesktopNativeBundle,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
type DesktopNativeBundle,
|
||||
type DesktopNativeLocale,
|
||||
@@ -70,6 +69,40 @@ const INTL: Record<Locale, string> = {
|
||||
sv: "sv-SE",
|
||||
}
|
||||
|
||||
const LABEL_KEY: Partial<Record<Locale, keyof Dictionary>> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
zht: "language.zht",
|
||||
ko: "language.ko",
|
||||
de: "language.de",
|
||||
es: "language.es",
|
||||
fr: "language.fr",
|
||||
da: "language.da",
|
||||
ja: "language.ja",
|
||||
pl: "language.pl",
|
||||
ru: "language.ru",
|
||||
uk: "language.uk",
|
||||
ar: "language.ar",
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
tr: "language.tr",
|
||||
}
|
||||
|
||||
const LABEL: Partial<Record<Locale, string>> = {
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
}
|
||||
|
||||
const base = i18n.flatten({ ...en, ...uiEn })
|
||||
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
||||
|
||||
@@ -239,7 +272,11 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
|
||||
}
|
||||
|
||||
const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
|
||||
const label = (value: Locale) => {
|
||||
const key = LABEL_KEY[value]
|
||||
if (key) return t(key)
|
||||
return LABEL[value] ?? value
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document !== "object") return
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
"command.session.export": "تصدير الجلسة",
|
||||
"command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON",
|
||||
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
@@ -534,8 +531,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "برنامج نصي لبدء تشغيل مساحة العمل",
|
||||
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "عدم الإظهار مرة أخرى",
|
||||
|
||||
"context.breakdown.title": "تفصيل السياق",
|
||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال المميزة. يشمل "أخرى" تعريفات الأدوات والأعباء الإضافية.',
|
||||
"context.breakdown.system": "النظام",
|
||||
@@ -545,8 +540,6 @@ export const dict = {
|
||||
"context.breakdown.other": "أخرى",
|
||||
"context.systemPrompt.title": "موجّه النظام",
|
||||
"context.rawMessages.title": "الرسائل الخام",
|
||||
"context.export.session": "تصدير الجلسة",
|
||||
|
||||
"context.stats.session": "جلسة",
|
||||
"context.stats.messages": "رسائل",
|
||||
"context.stats.provider": "موفر",
|
||||
@@ -613,11 +606,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
|
||||
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
|
||||
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
|
||||
"toast.session.export.success.title": "تم تصدير الجلسة",
|
||||
"toast.session.export.success.description": "تم حفظ الجلسة في \u2068{{filename}}\u2069",
|
||||
"toast.session.export.failed.title": "فشل تصدير الجلسة",
|
||||
"toast.session.export.failed.description": "حدث خطأ أثناء تصدير الجلسة",
|
||||
|
||||
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
|
||||
"toast.update.title": "تحديث متاح",
|
||||
"toast.update.description": "يتوفر الآن إصدار جديد من OpenCode ({{version}}) للتثبيت.",
|
||||
@@ -805,7 +793,6 @@ export const dict = {
|
||||
"common.moreOptions": "مزيد من الخيارات",
|
||||
"common.learnMore": "اطّلع على المزيد",
|
||||
"common.rename": "إعادة تسمية",
|
||||
"common.export": "تصدير",
|
||||
"common.reset": "إعادة تعيين",
|
||||
"common.archive": "أرشفة",
|
||||
"common.delete": "حذف",
|
||||
|
||||
@@ -180,9 +180,6 @@ export const dict = {
|
||||
"command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala",
|
||||
"command.session.unshare": "Sessiyanın paylaşımını dayandır",
|
||||
"command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır",
|
||||
"command.session.export": "Sessiyanı ixrac et",
|
||||
"command.session.export.description": "Sessiyanın tam transkriptini JSON formatında ixrac et",
|
||||
|
||||
"palette.search.placeholder": "Fayl, əmr və sessiya axtar",
|
||||
"palette.search.placeholder.home": "Əmr və sessiyaları axtar",
|
||||
"palette.empty": "Nəticə tapılmadı",
|
||||
@@ -544,8 +541,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Növbəti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Gələcəkdə göstərmə",
|
||||
"dialog.releaseNotes.media.alt": "Buraxılış önbaxışı",
|
||||
"dialog.usageExceeded.dontShowAgain": "Bir daha göstərmə",
|
||||
|
||||
"context.breakdown.title": "Kontekst bölgüsü",
|
||||
"context.breakdown.note": 'Giriş tokenlərinin təxmini bölgüsü. "Digər" alət təriflərini və əlavə yükü əhatə edir.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -555,8 +550,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Digər",
|
||||
"context.systemPrompt.title": "Sistem promptu",
|
||||
"context.rawMessages.title": "Xam mesajlar",
|
||||
"context.export.session": "Sessiyanı ixrac et",
|
||||
|
||||
"context.stats.session": "Sessiya",
|
||||
"context.stats.messages": "Mesajlar",
|
||||
"context.stats.provider": "Provayder",
|
||||
@@ -623,11 +616,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!",
|
||||
"toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi",
|
||||
"toast.session.unshare.failed.description": "Sessiyanın paylaşımını dayandırarkən xəta baş verdi",
|
||||
"toast.session.export.success.title": "Sessiya ixrac edildi",
|
||||
"toast.session.export.success.description": "Sessiya {{filename}} faylına saxlanıldı",
|
||||
"toast.session.export.failed.title": "Sessiya ixrac edilə bilmədi",
|
||||
"toast.session.export.failed.description": "Sessiyanı ixrac edərkən xəta baş verdi",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} üçün sessiyalar yüklənə bilmədi",
|
||||
"toast.project.reloadFailed.title": "{{project}} yenidən yüklənə bilmədi",
|
||||
"toast.update.title": "Yeniləmə mövcuddur",
|
||||
@@ -844,7 +832,6 @@ export const dict = {
|
||||
"common.moreOptions": "Daha çox seçim",
|
||||
"common.learnMore": "Ətraflı öyrən",
|
||||
"common.rename": "Adını dəyiş",
|
||||
"common.export": "İxrac et",
|
||||
"common.reset": "Sıfırla",
|
||||
"common.archive": "Arxivlə",
|
||||
"common.delete": "Sil",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
"command.session.export": "Exportar sessão",
|
||||
"command.session.export.description": "Exportar a transcrição completa da sessão como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
@@ -537,8 +534,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
|
||||
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "Ex.: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Não mostrar novamente",
|
||||
|
||||
"context.breakdown.title": "Detalhamento do contexto",
|
||||
"context.breakdown.note":
|
||||
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e sobrecarga.',
|
||||
@@ -549,8 +544,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Outros",
|
||||
"context.systemPrompt.title": "Prompt do sistema",
|
||||
"context.rawMessages.title": "Mensagens brutas",
|
||||
"context.export.session": "Exportar sessão",
|
||||
|
||||
"context.stats.session": "Sessão",
|
||||
"context.stats.messages": "Mensagens",
|
||||
"context.stats.provider": "Provedor",
|
||||
@@ -617,11 +610,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
|
||||
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
|
||||
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
|
||||
"toast.session.export.success.title": "Sessão exportada",
|
||||
"toast.session.export.success.description": "Sessão salva em {{filename}}",
|
||||
"toast.session.export.failed.title": "Falha ao exportar sessão",
|
||||
"toast.session.export.failed.description": "Ocorreu um erro ao exportar a sessão",
|
||||
|
||||
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
|
||||
"toast.update.title": "Atualização disponível",
|
||||
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
|
||||
@@ -808,7 +796,6 @@ export const dict = {
|
||||
"common.moreOptions": "Mais opções",
|
||||
"common.learnMore": "Saiba mais",
|
||||
"common.rename": "Renomear",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Redefinir",
|
||||
"common.archive": "Arquivar",
|
||||
"common.delete": "Excluir",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Ukini dijeljenje sesije",
|
||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||
|
||||
"command.session.export": "Izvezi sesiju",
|
||||
"command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu",
|
||||
|
||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||
"palette.empty": "Nema rezultata",
|
||||
@@ -568,8 +565,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Nemoj više prikazivati",
|
||||
|
||||
"context.breakdown.title": "Razlaganje konteksta",
|
||||
"context.breakdown.note":
|
||||
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatno opterećenje.',
|
||||
@@ -582,8 +577,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Sistemski prompt",
|
||||
"context.rawMessages.title": "Sirove poruke",
|
||||
|
||||
"context.export.session": "Izvezi sesiju",
|
||||
|
||||
"context.stats.session": "Sesija",
|
||||
"context.stats.messages": "Poruke",
|
||||
"context.stats.provider": "Provajder",
|
||||
@@ -662,11 +655,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
|
||||
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
|
||||
|
||||
"toast.session.export.success.title": "Sesija izvezena",
|
||||
"toast.session.export.success.description": "Sesija je sačuvana kao {{filename}}",
|
||||
"toast.session.export.failed.title": "Izvoz sesije nije uspio",
|
||||
"toast.session.export.failed.description": "Došlo je do greške prilikom izvoza sesije",
|
||||
|
||||
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
|
||||
|
||||
"toast.update.title": "Dostupno ažuriranje",
|
||||
@@ -868,7 +856,6 @@ export const dict = {
|
||||
"common.moreOptions": "Više opcija",
|
||||
"common.learnMore": "Saznaj više",
|
||||
"common.rename": "Preimenuj",
|
||||
"common.export": "Izvezi",
|
||||
"common.reset": "Vrati na početno stanje",
|
||||
"common.archive": "Arhiviraj",
|
||||
"common.delete": "Izbriši",
|
||||
|
||||
@@ -92,9 +92,6 @@ export const dict = {
|
||||
"command.session.unshare": "Stop deling af session",
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"command.session.export": "Eksportér session",
|
||||
"command.session.export.description": "Eksportér hele sessionsudskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||
"palette.search.placeholder.home": "Søg i kommandoer og sessioner",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
@@ -449,8 +446,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
|
||||
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Vis ikke igen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||
@@ -463,8 +458,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå beskeder",
|
||||
|
||||
"context.export.session": "Eksportér session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Beskeder",
|
||||
"context.stats.provider": "Udbyder",
|
||||
@@ -542,11 +535,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session",
|
||||
"toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling",
|
||||
|
||||
"toast.session.export.success.title": "Session eksporteret",
|
||||
"toast.session.export.success.description": "Sessionen blev gemt i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere session",
|
||||
"toast.session.export.failed.description": "Der opstod en fejl under eksport af sessionen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}",
|
||||
|
||||
"toast.update.title": "Opdatering tilgængelig",
|
||||
@@ -745,7 +733,6 @@ export const dict = {
|
||||
"common.moreOptions": "Flere muligheder",
|
||||
"common.learnMore": "Lær mere",
|
||||
"common.rename": "Omdøb",
|
||||
"common.export": "Eksportér",
|
||||
"common.reset": "Nulstil",
|
||||
"common.archive": "Arkivér",
|
||||
"common.delete": "Slet",
|
||||
|
||||
@@ -89,9 +89,6 @@ export const dict = {
|
||||
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
"command.session.export": "Sitzung exportieren",
|
||||
"command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren",
|
||||
|
||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
@@ -428,8 +425,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nicht mehr anzeigen",
|
||||
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||
@@ -440,8 +435,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Andere",
|
||||
"context.systemPrompt.title": "System-Prompt",
|
||||
"context.rawMessages.title": "Rohdaten der Nachrichten",
|
||||
"context.export.session": "Sitzung exportieren",
|
||||
|
||||
"context.stats.session": "Sitzung",
|
||||
"context.stats.messages": "Nachrichten",
|
||||
"context.stats.provider": "Anbieter",
|
||||
@@ -508,11 +501,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
|
||||
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
|
||||
"toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten",
|
||||
"toast.session.export.success.title": "Sitzung exportiert",
|
||||
"toast.session.export.success.description": "Sitzung unter {{filename}} gespeichert",
|
||||
"toast.session.export.failed.title": "Sitzung konnte nicht exportiert werden",
|
||||
"toast.session.export.failed.description": "Beim Exportieren der Sitzung ist ein Fehler aufgetreten",
|
||||
|
||||
"toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden",
|
||||
"toast.update.title": "Update verfügbar",
|
||||
"toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.",
|
||||
@@ -698,7 +686,6 @@ export const dict = {
|
||||
"common.moreOptions": "Weitere Optionen",
|
||||
"common.learnMore": "Mehr erfahren",
|
||||
"common.rename": "Umbenennen",
|
||||
"common.export": "Exportieren",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.archive": "Archivieren",
|
||||
"common.delete": "Löschen",
|
||||
|
||||
@@ -3,47 +3,12 @@ import {
|
||||
createDesktopNativeBundle,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_KEYS,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
|
||||
formatDesktopNativeMessage,
|
||||
parseDesktopNativeBundle,
|
||||
} from "./desktop-native"
|
||||
|
||||
describe("desktop native translations", () => {
|
||||
test("uses native language names independent of the active locale", () => {
|
||||
expect(DESKTOP_NATIVE_LOCALES.map((locale) => DESKTOP_NATIVE_LABELS[locale])).toEqual([
|
||||
"English",
|
||||
"简体中文",
|
||||
"繁體中文",
|
||||
"한국어",
|
||||
"Deutsch",
|
||||
"Español",
|
||||
"Français",
|
||||
"Dansk",
|
||||
"日本語",
|
||||
"Polski",
|
||||
"Русский",
|
||||
"Українська",
|
||||
"Bosanski",
|
||||
"العربية",
|
||||
"Norsk",
|
||||
"Português (Brasil)",
|
||||
"ไทย",
|
||||
"Türkçe",
|
||||
"हिन्दी",
|
||||
"Nederlands",
|
||||
"Bahasa Indonesia",
|
||||
"Tiếng Việt",
|
||||
"Italiano",
|
||||
"اردو",
|
||||
"پنجابی",
|
||||
"Azərbaycanca",
|
||||
"Suomi",
|
||||
"Svenska",
|
||||
])
|
||||
})
|
||||
|
||||
test("accepts the exact typed bundle", () => {
|
||||
const bundle = createDesktopNativeBundle("en", (key) => DESKTOP_NATIVE_ENGLISH[key])
|
||||
expect(parseDesktopNativeBundle(bundle)).toEqual(bundle)
|
||||
|
||||
@@ -31,37 +31,6 @@ export const DESKTOP_NATIVE_LOCALES = [
|
||||
|
||||
export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number]
|
||||
|
||||
export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
zht: "繁體中文",
|
||||
ko: "한국어",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
da: "Dansk",
|
||||
ja: "日本語",
|
||||
pl: "Polski",
|
||||
ru: "Русский",
|
||||
uk: "Українська",
|
||||
bs: "Bosanski",
|
||||
ar: "العربية",
|
||||
no: "Norsk",
|
||||
br: "Português (Brasil)",
|
||||
th: "ไทย",
|
||||
tr: "Türkçe",
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
}
|
||||
|
||||
export const DESKTOP_NATIVE_ENGLISH = {
|
||||
"desktop.menu.app": "OpenCode",
|
||||
"desktop.menu.file": "File",
|
||||
|
||||
@@ -95,8 +95,6 @@ export const dict = {
|
||||
"command.session.share.description": "Share this session and copy the URL to clipboard",
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.search.placeholder.home": "Search commands and sessions",
|
||||
@@ -480,7 +478,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Next",
|
||||
"dialog.releaseNotes.action.hideFuture": "Don't show these in the future",
|
||||
"dialog.releaseNotes.media.alt": "Release preview",
|
||||
"dialog.usageExceeded.dontShowAgain": "Don't show again",
|
||||
|
||||
"context.breakdown.title": "Context Breakdown",
|
||||
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
|
||||
@@ -492,7 +489,6 @@ export const dict = {
|
||||
|
||||
"context.systemPrompt.title": "System Prompt",
|
||||
"context.rawMessages.title": "Raw messages",
|
||||
"context.export.session": "Export session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
@@ -572,11 +568,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||
|
||||
"toast.session.export.success.title": "Session exported",
|
||||
"toast.session.export.success.description": "Saved session to {{filename}}",
|
||||
"toast.session.export.failed.title": "Failed to export session",
|
||||
"toast.session.export.failed.description": "An error occurred while exporting the session",
|
||||
|
||||
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
|
||||
|
||||
@@ -811,7 +802,6 @@ export const dict = {
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.export": "Export",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.delete": "Delete",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Dejar de compartir sesión",
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"command.session.export": "Exportar sesión",
|
||||
"command.session.export.description": "Exportar la transcripción completa de la sesión como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
@@ -570,8 +567,6 @@ export const dict = {
|
||||
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "No volver a mostrar",
|
||||
|
||||
"context.breakdown.title": "Desglose del contexto",
|
||||
"context.breakdown.note":
|
||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||
@@ -584,8 +579,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt del sistema",
|
||||
"context.rawMessages.title": "Mensajes en bruto",
|
||||
|
||||
"context.export.session": "Exportar sesión",
|
||||
|
||||
"context.stats.session": "Sesión",
|
||||
"context.stats.messages": "Mensajes",
|
||||
"context.stats.provider": "Proveedor",
|
||||
@@ -664,11 +657,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión",
|
||||
"toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión",
|
||||
|
||||
"toast.session.export.success.title": "Sesión exportada",
|
||||
"toast.session.export.success.description": "Sesión guardada en {{filename}}",
|
||||
"toast.session.export.failed.title": "No se pudo exportar la sesión",
|
||||
"toast.session.export.failed.description": "Se produjo un error al exportar la sesión",
|
||||
|
||||
"toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}",
|
||||
|
||||
"toast.update.title": "Actualización disponible",
|
||||
@@ -873,7 +861,6 @@ export const dict = {
|
||||
"common.moreOptions": "Más opciones",
|
||||
"common.learnMore": "Más información",
|
||||
"common.rename": "Renombrar",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.archive": "Archivar",
|
||||
"common.delete": "Eliminar",
|
||||
|
||||
@@ -85,9 +85,6 @@ export const dict = {
|
||||
"command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle",
|
||||
"command.session.unshare": "Peru istunnon jakaminen",
|
||||
"command.session.unshare.description": "Lopeta tämän istunnon jakaminen",
|
||||
"command.session.export": "Vie istunto",
|
||||
"command.session.export.description": "Vie istunnon koko transkriptio JSON-muodossa",
|
||||
|
||||
"palette.search.placeholder": "Hae tiedostoja, komentoja ja istuntoja",
|
||||
"palette.search.placeholder.home": "Hae komentoja ja istuntoja",
|
||||
"palette.empty": "Tuloksia ei löytynyt",
|
||||
@@ -432,8 +429,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Seuraava",
|
||||
"dialog.releaseNotes.action.hideFuture": "Älä näytä näitä jatkossa",
|
||||
"dialog.releaseNotes.media.alt": "Julkaisun esikatselu",
|
||||
"dialog.usageExceeded.dontShowAgain": "Älä näytä uudelleen",
|
||||
|
||||
"context.breakdown.title": "Kontekstin erittely",
|
||||
"context.breakdown.note":
|
||||
'Syötetokenien arvioitu jakautuminen. "Muut" sisältää työkalumäärittelyt ja muun oheissisällön.',
|
||||
@@ -444,8 +439,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Muut",
|
||||
"context.systemPrompt.title": "Järjestelmäkehote",
|
||||
"context.rawMessages.title": "Raakaviestit",
|
||||
"context.export.session": "Vie istunto",
|
||||
|
||||
"context.stats.session": "Istunto",
|
||||
"context.stats.messages": "Viestit",
|
||||
"context.stats.provider": "Palveluntarjoaja",
|
||||
@@ -512,11 +505,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!",
|
||||
"toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui",
|
||||
"toast.session.unshare.failed.description": "Virhe peruttaessa istunnon jakamista",
|
||||
"toast.session.export.success.title": "Istunto viety",
|
||||
"toast.session.export.success.description": "Istunto tallennettu tiedostoon {{filename}}",
|
||||
"toast.session.export.failed.title": "Istunnon vieminen epäonnistui",
|
||||
"toast.session.export.failed.description": "Istuntoa vietäessä tapahtui virhe",
|
||||
|
||||
"toast.session.listFailed.title": "Projektin {{project}} istuntojen lataaminen epäonnistui",
|
||||
"toast.project.reloadFailed.title": "Projektin {{project}} lataaminen uudelleen epäonnistui",
|
||||
"toast.update.title": "Päivitys saatavilla",
|
||||
@@ -734,7 +722,6 @@ export const dict = {
|
||||
"common.moreOptions": "Lisää vaihtoehtoja",
|
||||
"common.learnMore": "Lue lisää",
|
||||
"common.rename": "Nimeä uudelleen",
|
||||
"common.export": "Vie",
|
||||
"common.reset": "Palauta",
|
||||
"common.archive": "Arkistoi",
|
||||
"common.delete": "Poista",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
"command.session.export": "Exporter la session",
|
||||
"command.session.export.description": "Exporter la transcription intégrale de la session au format JSON",
|
||||
|
||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||
"palette.search.placeholder.home": "Rechercher des commandes et des sessions",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
@@ -543,8 +540,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Ne plus afficher",
|
||||
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les données annexes.",
|
||||
@@ -555,8 +550,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Autre",
|
||||
"context.systemPrompt.title": "Invite système",
|
||||
"context.rawMessages.title": "Messages bruts",
|
||||
"context.export.session": "Exporter la session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
"context.stats.provider": "Fournisseur",
|
||||
@@ -624,11 +617,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Échec de la désactivation du partage",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Une erreur s'est produite lors de la désactivation du partage de la session",
|
||||
"toast.session.export.success.title": "Session exportée",
|
||||
"toast.session.export.success.description": "Session enregistrée dans {{filename}}",
|
||||
"toast.session.export.failed.title": "Échec de l’exportation de la session",
|
||||
"toast.session.export.failed.description": "Une erreur s’est produite lors de l’exportation de la session",
|
||||
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
@@ -811,7 +799,6 @@ export const dict = {
|
||||
"common.moreOptions": "Plus d'options",
|
||||
"common.learnMore": "En savoir plus",
|
||||
"common.rename": "Renommer",
|
||||
"common.export": "Exporter",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.archive": "Archiver",
|
||||
"common.delete": "Supprimer",
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें",
|
||||
"command.session.unshare": "सेशन साझा करना बंद करें",
|
||||
"command.session.unshare.description": "इस सेशन को साझा करना बंद करें",
|
||||
"command.session.export": "सेशन निर्यात करें",
|
||||
"command.session.export.description": "सेशन की पूरी ट्रांसक्रिप्ट को JSON के रूप में निर्यात करें",
|
||||
|
||||
"palette.search.placeholder": "फ़ाइलें, कमांड और सेशन खोजें",
|
||||
"palette.search.placeholder.home": "कमांड और सेशन खोजें",
|
||||
"palette.empty": "कोई परिणाम नहीं मिला",
|
||||
@@ -546,8 +543,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "अगला",
|
||||
"dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ",
|
||||
"dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन",
|
||||
"dialog.usageExceeded.dontShowAgain": "फिर से न दिखाएँ",
|
||||
|
||||
"context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन",
|
||||
"context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।',
|
||||
"context.breakdown.system": "सिस्टम",
|
||||
@@ -557,8 +552,6 @@ export const dict = {
|
||||
"context.breakdown.other": "अन्य",
|
||||
"context.systemPrompt.title": "सिस्टम प्रॉम्प्ट",
|
||||
"context.rawMessages.title": "मूल संदेश",
|
||||
"context.export.session": "सेशन निर्यात करें",
|
||||
|
||||
"context.stats.session": "सेशन",
|
||||
"context.stats.messages": "संदेश",
|
||||
"context.stats.provider": "प्रोवाइडर",
|
||||
@@ -625,11 +618,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!",
|
||||
"toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल",
|
||||
"toast.session.unshare.failed.description": "सेशन को अनशेयर करते समय एक त्रुटि उत्पन्न हुई",
|
||||
"toast.session.export.success.title": "सेशन निर्यात किया गया",
|
||||
"toast.session.export.success.description": "सेशन को {{filename}} में सहेजा गया",
|
||||
"toast.session.export.failed.title": "सेशन निर्यात करने में विफल",
|
||||
"toast.session.export.failed.description": "सेशन निर्यात करते समय एक त्रुटि हुई",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} के लिए सेशन लोड करने में विफल",
|
||||
"toast.project.reloadFailed.title": "{{project}} को पुनः लोड करने में विफल",
|
||||
"toast.update.title": "उपलब्ध अद्यतन",
|
||||
@@ -845,7 +833,6 @@ export const dict = {
|
||||
"common.moreOptions": "अधिक विकल्प",
|
||||
"common.learnMore": "और अधिक जानें",
|
||||
"common.rename": "नाम बदलें",
|
||||
"common.export": "निर्यात करें",
|
||||
"common.reset": "रीसेट करें",
|
||||
"common.archive": "संग्रहित करें",
|
||||
"common.delete": "हटाएँ",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Hentikan berbagi",
|
||||
"command.session.unshare.description": "Hentikan berbagi sesi ini",
|
||||
|
||||
"command.session.export": "Ekspor sesi",
|
||||
"command.session.export.description": "Ekspor transkrip sesi lengkap sebagai JSON",
|
||||
|
||||
"palette.search.placeholder": "Cari berkas, perintah, dan sesi",
|
||||
"palette.search.placeholder.home": "Cari perintah dan sesi",
|
||||
"palette.empty": "Hasil tidak ditemukan",
|
||||
@@ -579,8 +576,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.hideFuture": "Jangan tampilkan ini di masa depan",
|
||||
"dialog.releaseNotes.media.alt": "Pratinjau rilis",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Jangan tampilkan lagi",
|
||||
|
||||
"context.breakdown.title": "Rincian Konteks",
|
||||
"context.breakdown.note": 'Perkiraan rincian token masukan. "Lainnya" mencakup definisi alat dan beban tambahan.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -592,8 +587,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt Sistem",
|
||||
"context.rawMessages.title": "Pesan mentah",
|
||||
|
||||
"context.export.session": "Ekspor sesi",
|
||||
|
||||
"context.stats.session": "Sesi",
|
||||
"context.stats.messages": "Pesan",
|
||||
"context.stats.provider": "Penyedia",
|
||||
@@ -671,11 +664,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Gagal menghentikan berbagi sesi",
|
||||
"toast.session.unshare.failed.description": "Terjadi kesalahan saat menghentikan berbagi sesi",
|
||||
|
||||
"toast.session.export.success.title": "Sesi diekspor",
|
||||
"toast.session.export.success.description": "Sesi disimpan ke {{filename}}",
|
||||
"toast.session.export.failed.title": "Gagal mengekspor sesi",
|
||||
"toast.session.export.failed.description": "Terjadi kesalahan saat mengekspor sesi",
|
||||
|
||||
"toast.session.listFailed.title": "Gagal memuat sesi untuk {{project}}",
|
||||
"toast.project.reloadFailed.title": "Gagal memuat ulang {{project}}",
|
||||
|
||||
@@ -909,7 +897,6 @@ export const dict = {
|
||||
"common.moreOptions": "Opsi lainnya",
|
||||
"common.learnMore": "Pelajari lebih lanjut",
|
||||
"common.rename": "Ganti nama",
|
||||
"common.export": "Ekspor",
|
||||
"common.reset": "Atur ulang",
|
||||
"common.archive": "Arsipkan",
|
||||
"common.delete": "Hapus",
|
||||
|
||||
@@ -86,9 +86,6 @@ export const dict = {
|
||||
"command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti",
|
||||
"command.session.unshare": "Annulla condivisione sessione",
|
||||
"command.session.unshare.description": "Interrompi la condivisione di questa sessione",
|
||||
"command.session.export": "Esporta sessione",
|
||||
"command.session.export.description": "Esporta la trascrizione completa della sessione in formato JSON",
|
||||
|
||||
"palette.search.placeholder": "Cerca file, comandi e sessioni",
|
||||
"palette.search.placeholder.home": "Cerca comandi e sessioni",
|
||||
"palette.empty": "Nessun risultato trovato",
|
||||
@@ -450,8 +447,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Avanti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Non mostrarli in futuro",
|
||||
"dialog.releaseNotes.media.alt": "Anteprima delle novità",
|
||||
"dialog.usageExceeded.dontShowAgain": "Non mostrare più",
|
||||
|
||||
"context.breakdown.title": "Ripartizione del contesto",
|
||||
"context.breakdown.note":
|
||||
'Ripartizione approssimativa dei token di input. "Altro" include le definizioni degli strumenti e i dati aggiuntivi.',
|
||||
@@ -462,8 +457,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Altro",
|
||||
"context.systemPrompt.title": "Prompt di sistema",
|
||||
"context.rawMessages.title": "Messaggi non elaborati",
|
||||
"context.export.session": "Esporta sessione",
|
||||
|
||||
"context.stats.session": "Sessione",
|
||||
"context.stats.messages": "Messaggi",
|
||||
"context.stats.provider": "Provider",
|
||||
@@ -531,11 +524,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Si è verificato un errore durante l'annullamento della condivisione della sessione",
|
||||
"toast.session.export.success.title": "Sessione esportata",
|
||||
"toast.session.export.success.description": "Sessione salvata in {{filename}}",
|
||||
"toast.session.export.failed.title": "Impossibile esportare la sessione",
|
||||
"toast.session.export.failed.description": "Si è verificato un errore durante l’esportazione della sessione",
|
||||
|
||||
"toast.session.listFailed.title": "Impossibile caricare le sessioni per {{project}}",
|
||||
"toast.project.reloadFailed.title": "Impossibile ricaricare {{project}}",
|
||||
"toast.update.title": "Aggiornamento disponibile",
|
||||
@@ -755,7 +743,6 @@ export const dict = {
|
||||
"common.moreOptions": "Altre opzioni",
|
||||
"common.learnMore": "Saperne di più",
|
||||
"common.rename": "Rinomina",
|
||||
"common.export": "Esporta",
|
||||
"common.reset": "Ripristina",
|
||||
"common.archive": "Archivia",
|
||||
"common.delete": "Elimina",
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
|
||||
"command.session.unshare": "セッションの共有を停止",
|
||||
"command.session.unshare.description": "このセッションの共有を停止",
|
||||
"command.session.export": "セッションをエクスポート",
|
||||
"command.session.export.description": "セッションの全記録を JSON としてエクスポート",
|
||||
|
||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||
"palette.empty": "結果が見つかりません",
|
||||
@@ -536,8 +533,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "今後表示しない",
|
||||
|
||||
"context.breakdown.title": "コンテキストの内訳",
|
||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||
"context.breakdown.system": "システム",
|
||||
@@ -547,8 +542,6 @@ export const dict = {
|
||||
"context.breakdown.other": "その他",
|
||||
"context.systemPrompt.title": "システムプロンプト",
|
||||
"context.rawMessages.title": "生のメッセージ",
|
||||
"context.export.session": "セッションをエクスポート",
|
||||
|
||||
"context.stats.session": "セッション",
|
||||
"context.stats.messages": "メッセージ",
|
||||
"context.stats.provider": "プロバイダー",
|
||||
@@ -615,11 +608,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "セッションの共有解除に成功しました!",
|
||||
"toast.session.unshare.failed.title": "セッションの共有解除に失敗しました",
|
||||
"toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました",
|
||||
"toast.session.export.success.title": "セッションをエクスポートしました",
|
||||
"toast.session.export.success.description": "セッションを {{filename}} に保存しました",
|
||||
"toast.session.export.failed.title": "セッションのエクスポートに失敗しました",
|
||||
"toast.session.export.failed.description": "セッションのエクスポート中にエラーが発生しました",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました",
|
||||
"toast.update.title": "アップデートが利用可能です",
|
||||
"toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。",
|
||||
@@ -793,7 +781,6 @@ export const dict = {
|
||||
"common.moreOptions": "その他のオプション",
|
||||
"common.learnMore": "詳細",
|
||||
"common.rename": "名前を変更",
|
||||
"common.export": "エクスポート",
|
||||
"common.reset": "リセット",
|
||||
"common.archive": "アーカイブ",
|
||||
"common.delete": "削除",
|
||||
|
||||
@@ -81,9 +81,6 @@ export const dict = {
|
||||
"command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사",
|
||||
"command.session.unshare": "세션 공유 중지",
|
||||
"command.session.unshare.description": "이 세션 공유 중지",
|
||||
"command.session.export": "세션 내보내기",
|
||||
"command.session.export.description": "전체 세션 기록을 JSON으로 내보내기",
|
||||
|
||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||
"palette.empty": "결과 없음",
|
||||
@@ -339,8 +336,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
|
||||
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "예: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "다시 표시하지 않기",
|
||||
|
||||
"context.breakdown.title": "컨텍스트 분석",
|
||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||
"context.breakdown.system": "시스템",
|
||||
@@ -350,8 +345,6 @@ export const dict = {
|
||||
"context.breakdown.other": "기타",
|
||||
"context.systemPrompt.title": "시스템 프롬프트",
|
||||
"context.rawMessages.title": "원시 메시지",
|
||||
"context.export.session": "세션 내보내기",
|
||||
|
||||
"context.stats.session": "세션",
|
||||
"context.stats.messages": "메시지",
|
||||
"context.stats.provider": "공급자",
|
||||
@@ -417,11 +410,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!",
|
||||
"toast.session.unshare.failed.title": "세션 공유 해제 실패",
|
||||
"toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다",
|
||||
"toast.session.export.success.title": "세션을 내보냈습니다",
|
||||
"toast.session.export.success.description": "세션 저장 위치: {{filename}}",
|
||||
"toast.session.export.failed.title": "세션 내보내기 실패",
|
||||
"toast.session.export.failed.description": "세션을 내보내는 동안 오류가 발생했습니다",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다",
|
||||
"toast.update.title": "업데이트 가능",
|
||||
"toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.",
|
||||
@@ -556,7 +544,6 @@ export const dict = {
|
||||
"common.moreOptions": "더 많은 옵션",
|
||||
"common.learnMore": "더 알아보기",
|
||||
"common.rename": "이름 바꾸기",
|
||||
"common.export": "내보내기",
|
||||
"common.reset": "초기화",
|
||||
"common.archive": "보관",
|
||||
"common.delete": "삭제",
|
||||
|
||||
@@ -178,9 +178,6 @@ export const dict = {
|
||||
"command.session.share.description": "Deel deze sessie en kopieer de URL naar het klembord",
|
||||
"command.session.unshare": "Sessie niet meer delen",
|
||||
"command.session.unshare.description": "Stop met het delen van deze sessie",
|
||||
"command.session.export": "Sessie exporteren",
|
||||
"command.session.export.description": "Het volledige sessietranscript exporteren als JSON",
|
||||
|
||||
"palette.search.placeholder": "Zoek naar bestanden, opdrachten en sessies",
|
||||
"palette.search.placeholder.home": "Opdrachten en sessies zoeken",
|
||||
"palette.empty": "Geen resultaten gevonden",
|
||||
@@ -542,8 +539,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Volgende",
|
||||
"dialog.releaseNotes.action.hideFuture": "Laat deze in de toekomst niet zien",
|
||||
"dialog.releaseNotes.media.alt": "Releasevoorbeeld",
|
||||
"dialog.usageExceeded.dontShowAgain": "Niet meer weergeven",
|
||||
|
||||
"context.breakdown.title": "Contextanalyse",
|
||||
"context.breakdown.note":
|
||||
'Geschatte uitsplitsing van invoertokens. "Overig" omvat gereedschapsdefinities en overhead.',
|
||||
@@ -554,8 +549,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Overig",
|
||||
"context.systemPrompt.title": "Systeemprompt",
|
||||
"context.rawMessages.title": "Ruwe berichten",
|
||||
"context.export.session": "Sessie exporteren",
|
||||
|
||||
"context.stats.session": "Sessie",
|
||||
"context.stats.messages": "Berichten",
|
||||
"context.stats.provider": "Aanbieder",
|
||||
@@ -623,11 +616,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kan het delen van de sessie niet ongedaan maken",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Er is een fout opgetreden bij het ongedaan maken van het delen van de sessie",
|
||||
"toast.session.export.success.title": "Sessie geëxporteerd",
|
||||
"toast.session.export.success.description": "Sessie opgeslagen in {{filename}}",
|
||||
"toast.session.export.failed.title": "Kan sessie niet exporteren",
|
||||
"toast.session.export.failed.description": "Er is een fout opgetreden tijdens het exporteren van de sessie",
|
||||
|
||||
"toast.session.listFailed.title": "Kan sessies voor {{project}} niet laden",
|
||||
"toast.project.reloadFailed.title": "Kan {{project}} niet opnieuw laden",
|
||||
"toast.update.title": "Update beschikbaar",
|
||||
@@ -844,7 +832,6 @@ export const dict = {
|
||||
"common.moreOptions": "Meer opties",
|
||||
"common.learnMore": "Meer informatie",
|
||||
"common.rename": "Hernoemen",
|
||||
"common.export": "Exporteren",
|
||||
"common.reset": "Opnieuw instellen",
|
||||
"common.archive": "Archiveren",
|
||||
"common.delete": "Verwijderen",
|
||||
|
||||
@@ -191,9 +191,6 @@ export const dict = {
|
||||
"command.session.unshare": "Slutt å dele sesjon",
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"command.session.export": "Eksporter sesjon",
|
||||
"command.session.export.description": "Eksporter hele sesjonsutskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
@@ -473,8 +470,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Ikke vis igjen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling av inndatatokener. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||
@@ -487,8 +482,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå meldinger",
|
||||
|
||||
"context.export.session": "Eksporter sesjon",
|
||||
|
||||
"context.stats.session": "Sesjon",
|
||||
"context.stats.messages": "Meldinger",
|
||||
"context.stats.provider": "Leverandør",
|
||||
@@ -566,11 +559,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
|
||||
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
|
||||
|
||||
"toast.session.export.success.title": "Sesjon eksportert",
|
||||
"toast.session.export.success.description": "Sesjonen ble lagret i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere sesjon",
|
||||
"toast.session.export.failed.description": "Det oppstod en feil under eksport av sesjonen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
|
||||
|
||||
"toast.update.title": "Oppdatering tilgjengelig",
|
||||
@@ -733,7 +721,6 @@ export const dict = {
|
||||
"common.moreOptions": "Flere alternativer",
|
||||
"common.learnMore": "Lær mer",
|
||||
"common.rename": "Gi nytt navn",
|
||||
"common.export": "Eksporter",
|
||||
"common.reset": "Tilbakestill",
|
||||
"common.archive": "Arkiver",
|
||||
"common.delete": "Slett",
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "اس سیشن نو شیئر کرو تے URL نو کلپ بورڈ تے کاپی کرو",
|
||||
"command.session.unshare": "سیشن شیئر کرنا بند کرو",
|
||||
"command.session.unshare.description": "اس سیشن نو شیئر کرنا بند کرو",
|
||||
"command.session.export": "سیشن برآمد کرو",
|
||||
"command.session.export.description": "سیشن دی پوری نقل JSON دی شکل وچ برآمد کرو",
|
||||
|
||||
"palette.search.placeholder": "فائلاں، کمانڈز، تے سیشنز تلاش کرو",
|
||||
"palette.search.placeholder.home": "سرچ کمانڈز تے سیشنز",
|
||||
"palette.empty": "کوئی نتیجہ نئیں ملیا",
|
||||
@@ -547,8 +544,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "اگلا",
|
||||
"dialog.releaseNotes.action.hideFuture": "ایہہ اگوں نہ وکھاؤ",
|
||||
"dialog.releaseNotes.media.alt": "ریلیز پیش نظارہ",
|
||||
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ وکھاؤ",
|
||||
|
||||
"context.breakdown.title": "کانٹیکسٹ دی ونڈ",
|
||||
"context.breakdown.note": 'ان پٹ ٹوکناں دی لگ بھگ ونڈ۔ "ہور" وچ ٹول دیاں تعریفاں تے وادھو خرچ شامل نیں۔',
|
||||
"context.breakdown.system": "نظام",
|
||||
@@ -558,8 +553,6 @@ export const dict = {
|
||||
"context.breakdown.other": "ہور",
|
||||
"context.systemPrompt.title": "سسٹم پرامپٹ",
|
||||
"context.rawMessages.title": "خام سنیہے",
|
||||
"context.export.session": "سیشن برآمد کرو",
|
||||
|
||||
"context.stats.session": "سیشن",
|
||||
"context.stats.messages": "سنیہے",
|
||||
"context.stats.provider": "پرووائیڈر",
|
||||
@@ -626,11 +619,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "سیشن دا شیئر کامیابی نال بند ہو گیا!",
|
||||
"toast.session.unshare.failed.title": "سیشن ان شیئر کرن چ ناکام رہیا",
|
||||
"toast.session.unshare.failed.description": "سیشن ان شیئر کردے ویلے کوئی غلطی ہو گئی",
|
||||
"toast.session.export.success.title": "سیشن برآمد ہو گیا",
|
||||
"toast.session.export.success.description": "سیشن نوں \u2068{{filename}}\u2069 وچ محفوظ کر دتا گیا",
|
||||
"toast.session.export.failed.title": "سیشن برآمد کرن چ ناکامی ہوئی",
|
||||
"toast.session.export.failed.description": "سیشن برآمد کردے ویلے اک غلطی ہو گئی",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} لئی سیشن لوڈ کرن چ ناکام رہیا",
|
||||
"toast.project.reloadFailed.title": "{{project}} دوبارہ لوڈ نئیں ہو سکیا",
|
||||
"toast.update.title": "اپ ڈیٹ دستیاب اے",
|
||||
@@ -843,7 +831,6 @@ export const dict = {
|
||||
"common.moreOptions": "ہور اختیارات",
|
||||
"common.learnMore": "ہور جانو",
|
||||
"common.rename": "ناں بدلو",
|
||||
"common.export": "برآمد کرو",
|
||||
"common.reset": "ری سیٹ کرو",
|
||||
"common.archive": "آرکائیو کرو",
|
||||
"common.delete": "مکاؤ",
|
||||
|
||||
@@ -63,7 +63,7 @@ const domains = [
|
||||
},
|
||||
] as const
|
||||
|
||||
describe("i18n parity", () => {
|
||||
describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
||||
test("non-English locales have every English key and required plural variants", async () => {
|
||||
for (const domain of domains) {
|
||||
const source = await dictionary(domain.source)
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "Udostępnij tę sesję i skopiuj adres URL do schowka",
|
||||
"command.session.unshare": "Przestań udostępniać sesję",
|
||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||
"command.session.export": "Eksportuj sesję",
|
||||
"command.session.export.description": "Eksportuj pełny zapis sesji w formacie JSON",
|
||||
|
||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||
"palette.empty": "Brak wyników",
|
||||
@@ -538,8 +535,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "np. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nie pokazuj ponownie",
|
||||
|
||||
"context.breakdown.title": "Podział kontekstu",
|
||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||
"context.breakdown.system": "System",
|
||||
@@ -549,8 +544,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Inne",
|
||||
"context.systemPrompt.title": "Prompt systemowy",
|
||||
"context.rawMessages.title": "Surowe wiadomości",
|
||||
"context.export.session": "Eksportuj sesję",
|
||||
|
||||
"context.stats.session": "Sesja",
|
||||
"context.stats.messages": "Wiadomości",
|
||||
"context.stats.provider": "Dostawca",
|
||||
@@ -617,11 +610,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!",
|
||||
"toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji",
|
||||
"toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji",
|
||||
"toast.session.export.success.title": "Sesja wyeksportowana",
|
||||
"toast.session.export.success.description": "Sesję zapisano jako {{filename}}",
|
||||
"toast.session.export.failed.title": "Nie udało się wyeksportować sesji",
|
||||
"toast.session.export.failed.description": "Wystąpił błąd podczas eksportowania sesji",
|
||||
|
||||
"toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}",
|
||||
"toast.update.title": "Dostępna aktualizacja",
|
||||
"toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.",
|
||||
@@ -809,7 +797,6 @@ export const dict = {
|
||||
"common.moreOptions": "Więcej opcji",
|
||||
"common.learnMore": "Dowiedz się więcej",
|
||||
"common.rename": "Zmień nazwę",
|
||||
"common.export": "Eksportuj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.archive": "Archiwizuj",
|
||||
"common.delete": "Usuń",
|
||||
|
||||
@@ -192,9 +192,6 @@ export const dict = {
|
||||
"command.session.unshare": "Отменить публикацию",
|
||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||
|
||||
"command.session.export": "Экспортировать сессию",
|
||||
"command.session.export.description": "Экспортировать полную историю сессии в формате JSON",
|
||||
|
||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||
"palette.empty": "Ничего не найдено",
|
||||
@@ -567,8 +564,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Запускается после создания нового рабочего пространства (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "например, bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Больше не показывать",
|
||||
|
||||
"context.breakdown.title": "Разбивка контекста",
|
||||
"context.breakdown.note":
|
||||
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
||||
@@ -581,8 +576,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Системный промпт",
|
||||
"context.rawMessages.title": "Исходные сообщения",
|
||||
|
||||
"context.export.session": "Экспортировать сессию",
|
||||
|
||||
"context.stats.session": "Сессия",
|
||||
"context.stats.messages": "Сообщения",
|
||||
"context.stats.provider": "Провайдер",
|
||||
@@ -660,11 +653,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Не удалось отменить публикацию",
|
||||
"toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации",
|
||||
|
||||
"toast.session.export.success.title": "Сессия экспортирована",
|
||||
"toast.session.export.success.description": "Сессия сохранена в файл {{filename}}",
|
||||
"toast.session.export.failed.title": "Не удалось экспортировать сессию",
|
||||
"toast.session.export.failed.description": "Произошла ошибка при экспорте сессии",
|
||||
|
||||
"toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}",
|
||||
|
||||
"toast.update.title": "Доступно обновление",
|
||||
@@ -870,7 +858,6 @@ export const dict = {
|
||||
"common.moreOptions": "Дополнительные опции",
|
||||
"common.learnMore": "Подробнее",
|
||||
"common.rename": "Переименовать",
|
||||
"common.export": "Экспортировать",
|
||||
"common.reset": "Сбросить",
|
||||
"common.archive": "Архивировать",
|
||||
"common.delete": "Удалить",
|
||||
|
||||
@@ -179,9 +179,6 @@ export const dict = {
|
||||
"command.session.share.description": "Dela den här sessionen och kopiera URL:en till urklipp",
|
||||
"command.session.unshare": "Sluta dela session",
|
||||
"command.session.unshare.description": "Sluta dela den här sessionen",
|
||||
"command.session.export": "Exportera session",
|
||||
"command.session.export.description": "Exportera hela sessionsutskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Sök efter filer, kommandon och sessioner",
|
||||
"palette.search.placeholder.home": "Sök bland kommandon och sessioner",
|
||||
"palette.empty": "Inga resultat hittades",
|
||||
@@ -541,8 +538,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Nästa",
|
||||
"dialog.releaseNotes.action.hideFuture": "Visa inte dessa i framtiden",
|
||||
"dialog.releaseNotes.media.alt": "Förhandsvisning av version",
|
||||
"dialog.usageExceeded.dontShowAgain": "Visa inte igen",
|
||||
|
||||
"context.breakdown.title": "Kontextfördelning",
|
||||
"context.breakdown.note":
|
||||
'Ungefärlig uppdelning av inmatningstokens. "Övrigt" inkluderar verktygsdefinitioner och overhead.',
|
||||
@@ -553,8 +548,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Övrigt",
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Råmeddelanden",
|
||||
"context.export.session": "Exportera session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Meddelanden",
|
||||
"context.stats.provider": "Leverantör",
|
||||
@@ -621,11 +614,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Delningen av sessionen har avslutats!",
|
||||
"toast.session.unshare.failed.title": "Det gick inte att avsluta delningen",
|
||||
"toast.session.unshare.failed.description": "Ett fel uppstod när delningen skulle avslutas",
|
||||
"toast.session.export.success.title": "Session exporterad",
|
||||
"toast.session.export.success.description": "Sessionen sparades i {{filename}}",
|
||||
"toast.session.export.failed.title": "Det gick inte att exportera sessionen",
|
||||
"toast.session.export.failed.description": "Ett fel uppstod när sessionen exporterades",
|
||||
|
||||
"toast.session.listFailed.title": "Det gick inte att läsa in sessioner för {{project}}",
|
||||
"toast.project.reloadFailed.title": "Det gick inte att ladda om {{project}}",
|
||||
"toast.update.title": "Uppdatering tillgänglig",
|
||||
@@ -840,7 +828,6 @@ export const dict = {
|
||||
"common.moreOptions": "Fler alternativ",
|
||||
"common.learnMore": "Läs mer",
|
||||
"common.rename": "Byt namn",
|
||||
"common.export": "Exportera",
|
||||
"common.reset": "Återställ",
|
||||
"common.archive": "Arkivera",
|
||||
"common.delete": "Radera",
|
||||
|
||||
@@ -191,9 +191,6 @@ export const dict = {
|
||||
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||
|
||||
"command.session.export": "ส่งออกเซสชัน",
|
||||
"command.session.export.description": "ส่งออกบันทึกทั้งหมดของเซสชันเป็น JSON",
|
||||
|
||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
|
||||
"palette.empty": "ไม่พบผลลัพธ์",
|
||||
@@ -564,8 +561,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "เช่น bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "ไม่ต้องแสดงอีก",
|
||||
|
||||
"context.breakdown.title": "การแบ่งบริบท",
|
||||
"context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด',
|
||||
"context.breakdown.system": "ระบบ",
|
||||
@@ -577,8 +572,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "พรอมต์ระบบ",
|
||||
"context.rawMessages.title": "ข้อความดิบ",
|
||||
|
||||
"context.export.session": "ส่งออกเซสชัน",
|
||||
|
||||
"context.stats.session": "เซสชัน",
|
||||
"context.stats.messages": "ข้อความ",
|
||||
"context.stats.provider": "ผู้ให้บริการ",
|
||||
@@ -657,11 +650,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน",
|
||||
"toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน",
|
||||
|
||||
"toast.session.export.success.title": "ส่งออกเซสชันแล้ว",
|
||||
"toast.session.export.success.description": "บันทึกเซสชันไปยัง {{filename}} แล้ว",
|
||||
"toast.session.export.failed.title": "ไม่สามารถส่งออกเซสชัน",
|
||||
"toast.session.export.failed.description": "เกิดข้อผิดพลาดขณะส่งออกเซสชัน",
|
||||
|
||||
"toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}",
|
||||
|
||||
"toast.update.title": "มีการอัปเดต",
|
||||
@@ -855,7 +843,6 @@ export const dict = {
|
||||
"common.moreOptions": "ตัวเลือกเพิ่มเติม",
|
||||
"common.learnMore": "เรียนรู้เพิ่มเติม",
|
||||
"common.rename": "เปลี่ยนชื่อ",
|
||||
"common.export": "ส่งออก",
|
||||
"common.reset": "รีเซ็ต",
|
||||
"common.archive": "จัดเก็บ",
|
||||
"common.delete": "ลบ",
|
||||
|
||||
@@ -197,9 +197,6 @@ export const dict = {
|
||||
"command.session.unshare": "Paylaşımı kaldır",
|
||||
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
||||
|
||||
"command.session.export": "Oturumu dışa aktar",
|
||||
"command.session.export.description": "Oturumun tam dökümünü JSON olarak dışa aktar",
|
||||
|
||||
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
||||
"palette.search.placeholder.home": "Komut ve oturum ara",
|
||||
"palette.empty": "Sonuç bulunamadı",
|
||||
@@ -572,8 +569,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "örneğin bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Bir daha gösterme",
|
||||
|
||||
"context.breakdown.title": "Bağlam Dökümü",
|
||||
"context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -585,8 +580,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Sistem İstemi",
|
||||
"context.rawMessages.title": "Ham mesajlar",
|
||||
|
||||
"context.export.session": "Oturumu dışa aktar",
|
||||
|
||||
"context.stats.session": "Oturum",
|
||||
"context.stats.messages": "Mesajlar",
|
||||
"context.stats.provider": "Sağlayıcı",
|
||||
@@ -665,11 +658,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı",
|
||||
"toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu",
|
||||
|
||||
"toast.session.export.success.title": "Oturum dışa aktarıldı",
|
||||
"toast.session.export.success.description": "Oturum {{filename}} dosyasına kaydedildi",
|
||||
"toast.session.export.failed.title": "Oturum dışa aktarılamadı",
|
||||
"toast.session.export.failed.description": "Oturum dışa aktarılırken bir hata oluştu",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi",
|
||||
|
||||
"toast.update.title": "Güncelleme mevcut",
|
||||
@@ -873,7 +861,6 @@ export const dict = {
|
||||
"common.moreOptions": "Daha fazla seçenek",
|
||||
"common.learnMore": "Daha fazla bilgi",
|
||||
"common.rename": "Yeniden adlandır",
|
||||
"common.export": "Dışa aktar",
|
||||
"common.reset": "Sıfırla",
|
||||
"common.archive": "Arşivle",
|
||||
"common.delete": "Sil",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Припинити поширення сесії",
|
||||
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
||||
|
||||
"command.session.export": "Експортувати сесію",
|
||||
"command.session.export.description": "Експортувати повну історію сесії у форматі JSON",
|
||||
|
||||
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
||||
"palette.search.placeholder.home": "Пошук команд і сесій",
|
||||
"palette.empty": "Результатів не знайдено",
|
||||
@@ -581,8 +578,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому",
|
||||
"dialog.releaseNotes.media.alt": "Попередній перегляд релізу",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Більше не показувати",
|
||||
|
||||
"context.breakdown.title": "Розподіл контексту",
|
||||
"context.breakdown.note":
|
||||
'Приблизний розподіл вхідних токенів. "Інше" включає визначення інструментів і накладні витрати.',
|
||||
@@ -595,8 +590,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Системний запит",
|
||||
"context.rawMessages.title": "Необроблені повідомлення",
|
||||
|
||||
"context.export.session": "Експортувати сесію",
|
||||
|
||||
"context.stats.session": "Сесія",
|
||||
"context.stats.messages": "Повідомлення",
|
||||
"context.stats.provider": "Провайдер",
|
||||
@@ -675,11 +668,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Не вдалося припинити поширення сесії",
|
||||
"toast.session.unshare.failed.description": "Під час припинення поширення сесії сталася помилка",
|
||||
|
||||
"toast.session.export.success.title": "Сесію експортовано",
|
||||
"toast.session.export.success.description": "Сесію збережено у файл {{filename}}",
|
||||
"toast.session.export.failed.title": "Не вдалося експортувати сесію",
|
||||
"toast.session.export.failed.description": "Під час експортування сесії сталася помилка",
|
||||
|
||||
"toast.session.listFailed.title": "Не вдалося завантажити сесії для {{project}}",
|
||||
"toast.project.reloadFailed.title": "Не вдалося перезавантажити {{project}}",
|
||||
|
||||
@@ -922,7 +910,6 @@ export const dict = {
|
||||
"common.moreOptions": "Більше параметрів",
|
||||
"common.learnMore": "Дізнатися більше",
|
||||
"common.rename": "Перейменувати",
|
||||
"common.export": "Експортувати",
|
||||
"common.reset": "Скинути",
|
||||
"common.archive": "Архівувати",
|
||||
"common.delete": "Видалити",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "اس سیشن کا اشتراک کریں اور URL کو کلپ بورڈ میں کاپی کریں۔",
|
||||
"command.session.unshare": "سیشن کا اشتراک ختم کریں۔",
|
||||
"command.session.unshare.description": "اس سیشن کا اشتراک کرنا بند کریں۔",
|
||||
"command.session.export": "سیشن برآمد کریں",
|
||||
"command.session.export.description": "سیشن کی مکمل نقل JSON کی صورت میں برآمد کریں",
|
||||
|
||||
"palette.search.placeholder": "فائلیں، کمانڈز اور سیشنز تلاش کریں۔",
|
||||
"palette.search.placeholder.home": "کمانڈز اور سیشن تلاش کریں۔",
|
||||
"palette.empty": "کوئی نتیجہ نہیں ملا",
|
||||
@@ -549,8 +546,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "اگلا",
|
||||
"dialog.releaseNotes.action.hideFuture": "مستقبل میں یہ نہ دکھائیں۔",
|
||||
"dialog.releaseNotes.media.alt": "ریلیز کا پیش منظر",
|
||||
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ دکھائیں",
|
||||
|
||||
"context.breakdown.title": "سیاق و سباق کی تفصیل",
|
||||
"context.breakdown.note": 'ان پٹ ٹوکنز کی تخمینی تقسیم۔ "دیگر" میں ٹول کی تعریفیں اور اضافی بوجھ شامل ہیں۔',
|
||||
"context.breakdown.system": "سسٹم",
|
||||
@@ -560,8 +555,6 @@ export const dict = {
|
||||
"context.breakdown.other": "دیگر",
|
||||
"context.systemPrompt.title": "سسٹم پرامپٹ",
|
||||
"context.rawMessages.title": "خام پیغامات",
|
||||
"context.export.session": "سیشن برآمد کریں",
|
||||
|
||||
"context.stats.session": "سیشن",
|
||||
"context.stats.messages": "پیغامات",
|
||||
"context.stats.provider": "فراہم کنندہ",
|
||||
@@ -628,11 +621,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "سیشن کا اشتراک کامیابی سے ختم ہو گیا!",
|
||||
"toast.session.unshare.failed.title": "سیشن کا اشتراک ختم کرنے میں ناکام",
|
||||
"toast.session.unshare.failed.description": "سیشن کا اشتراک ختم کرتے وقت ایک خرابی پیش آگئی",
|
||||
"toast.session.export.success.title": "سیشن برآمد کر دیا گیا",
|
||||
"toast.session.export.success.description": "سیشن کو \u2068{{filename}}\u2069 میں محفوظ کر دیا گیا",
|
||||
"toast.session.export.failed.title": "سیشن برآمد کرنے میں ناکام",
|
||||
"toast.session.export.failed.description": "سیشن برآمد کرتے وقت ایک خرابی پیش آ گئی",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} کے لیے سیشن لوڈ کرنے میں ناکام",
|
||||
"toast.project.reloadFailed.title": "{{project}} کو دوبارہ لوڈ کرنے میں ناکام",
|
||||
"toast.update.title": "اپ ڈیٹ دستیاب ہے۔",
|
||||
@@ -846,7 +834,6 @@ export const dict = {
|
||||
"common.moreOptions": "مزید اختیارات",
|
||||
"common.learnMore": "مزید جانیں",
|
||||
"common.rename": "نام تبدیل کریں۔",
|
||||
"common.export": "برآمد کریں",
|
||||
"common.reset": "دوبارہ ترتیب دیں۔",
|
||||
"common.archive": "آرکائیو کریں",
|
||||
"common.delete": "حذف کریں۔",
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "Chia sẻ phiên này và sao chép URL vào bảng tạm",
|
||||
"command.session.unshare": "Ngừng chia sẻ phiên",
|
||||
"command.session.unshare.description": "Dừng chia sẻ phiên này",
|
||||
"command.session.export": "Xuất phiên",
|
||||
"command.session.export.description": "Xuất toàn bộ bản ghi phiên dưới dạng JSON",
|
||||
|
||||
"palette.search.placeholder": "Tìm kiếm tệp, lệnh và phiên",
|
||||
"palette.search.placeholder.home": "Tìm kiếm lệnh và phiên",
|
||||
"palette.empty": "Không tìm thấy kết quả nào",
|
||||
@@ -546,8 +543,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Tiếp theo",
|
||||
"dialog.releaseNotes.action.hideFuture": "Không hiển thị lại",
|
||||
"dialog.releaseNotes.media.alt": "Xem trước bản phát hành",
|
||||
"dialog.usageExceeded.dontShowAgain": "Không hiển thị lại",
|
||||
|
||||
"context.breakdown.title": "Phân tích ngữ cảnh",
|
||||
"context.breakdown.note":
|
||||
'Phân tích gần đúng của token đầu vào. "Khác" bao gồm các định nghĩa công cụ và chi phí chung.',
|
||||
@@ -558,8 +553,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Khác",
|
||||
"context.systemPrompt.title": "Lời nhắc hệ thống",
|
||||
"context.rawMessages.title": "Tin nhắn thô",
|
||||
"context.export.session": "Xuất phiên",
|
||||
|
||||
"context.stats.session": "Phiên",
|
||||
"context.stats.messages": "Tin nhắn",
|
||||
"context.stats.provider": "Nhà cung cấp",
|
||||
@@ -626,11 +619,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Đã hủy chia sẻ phiên thành công!",
|
||||
"toast.session.unshare.failed.title": "Không hủy chia sẻ được phiên",
|
||||
"toast.session.unshare.failed.description": "Đã xảy ra lỗi khi hủy chia sẻ phiên",
|
||||
"toast.session.export.success.title": "Đã xuất phiên",
|
||||
"toast.session.export.success.description": "Đã lưu phiên vào {{filename}}",
|
||||
"toast.session.export.failed.title": "Không thể xuất phiên",
|
||||
"toast.session.export.failed.description": "Đã xảy ra lỗi khi xuất phiên",
|
||||
|
||||
"toast.session.listFailed.title": "Không thể tải phiên cho {{project}}",
|
||||
"toast.project.reloadFailed.title": "Không thể tải lại {{project}}",
|
||||
"toast.update.title": "Đã có bản cập nhật",
|
||||
@@ -846,7 +834,6 @@ export const dict = {
|
||||
"common.moreOptions": "Nhiều lựa chọn hơn",
|
||||
"common.learnMore": "Tìm hiểu thêm",
|
||||
"common.rename": "Đổi tên",
|
||||
"common.export": "Xuất",
|
||||
"common.reset": "Đặt lại",
|
||||
"common.archive": "Lưu trữ",
|
||||
"common.delete": "Xóa",
|
||||
|
||||
@@ -218,9 +218,6 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享会话",
|
||||
"command.session.unshare.description": "停止分享此会话",
|
||||
|
||||
"command.session.export": "导出会话",
|
||||
"command.session.export.description": "将完整会话记录导出为 JSON",
|
||||
|
||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||
"palette.search.placeholder.home": "搜索命令和会话",
|
||||
"palette.empty": "未找到结果",
|
||||
@@ -580,8 +577,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "创建新工作区 (worktree) 后运行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "不再显示",
|
||||
|
||||
"context.breakdown.title": "上下文细分",
|
||||
"context.breakdown.note": "输入令牌的大致细分。“其他”包含工具定义和开销。",
|
||||
"context.breakdown.system": "系统",
|
||||
@@ -591,8 +586,6 @@ export const dict = {
|
||||
"context.breakdown.other": "其他",
|
||||
"context.systemPrompt.title": "系统提示词",
|
||||
"context.rawMessages.title": "原始消息",
|
||||
"context.export.session": "导出会话",
|
||||
|
||||
"context.stats.session": "会话",
|
||||
"context.stats.messages": "消息数",
|
||||
"context.stats.provider": "提供商",
|
||||
@@ -661,11 +654,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "会话已成功取消分享",
|
||||
"toast.session.unshare.failed.title": "取消分享失败",
|
||||
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
|
||||
"toast.session.export.success.title": "会话已导出",
|
||||
"toast.session.export.success.description": "已将会话保存到 {{filename}}",
|
||||
"toast.session.export.failed.title": "导出会话失败",
|
||||
"toast.session.export.failed.description": "导出会话时发生错误",
|
||||
|
||||
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
|
||||
"toast.update.title": "有可用更新",
|
||||
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
|
||||
@@ -851,7 +839,6 @@ export const dict = {
|
||||
"common.moreOptions": "更多选项",
|
||||
"common.learnMore": "了解更多",
|
||||
"common.rename": "重命名",
|
||||
"common.export": "导出",
|
||||
"common.reset": "重置",
|
||||
"common.archive": "归档",
|
||||
"common.delete": "删除",
|
||||
|
||||
@@ -195,9 +195,6 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享工作階段",
|
||||
"command.session.unshare.description": "停止分享此工作階段",
|
||||
|
||||
"command.session.export": "匯出工作階段",
|
||||
"command.session.export.description": "將完整的工作階段記錄匯出為 JSON",
|
||||
|
||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||
"palette.search.placeholder.home": "搜尋命令和工作階段",
|
||||
"palette.empty": "找不到結果",
|
||||
@@ -561,8 +558,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "工作區啟動腳本",
|
||||
"dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "不再顯示",
|
||||
|
||||
"context.breakdown.title": "上下文細分",
|
||||
"context.breakdown.note": "輸入 token 的概略細分。「其他」包含工具定義和額外負擔。",
|
||||
"context.breakdown.system": "系統",
|
||||
@@ -574,8 +569,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "系統提示詞",
|
||||
"context.rawMessages.title": "原始訊息",
|
||||
|
||||
"context.export.session": "匯出工作階段",
|
||||
|
||||
"context.stats.session": "工作階段",
|
||||
"context.stats.messages": "訊息數",
|
||||
"context.stats.provider": "提供者",
|
||||
@@ -653,11 +646,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "取消分享失敗",
|
||||
"toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤",
|
||||
|
||||
"toast.session.export.success.title": "工作階段已匯出",
|
||||
"toast.session.export.success.description": "已將工作階段儲存至 {{filename}}",
|
||||
"toast.session.export.failed.title": "匯出工作階段失敗",
|
||||
"toast.session.export.failed.description": "匯出工作階段時發生錯誤",
|
||||
|
||||
"toast.session.listFailed.title": "無法載入 {{project}} 的工作階段",
|
||||
|
||||
"toast.update.title": "有可用更新",
|
||||
@@ -848,7 +836,6 @@ export const dict = {
|
||||
"common.moreOptions": "更多選項",
|
||||
"common.learnMore": "深入了解",
|
||||
"common.rename": "重新命名",
|
||||
"common.export": "匯出",
|
||||
"common.reset": "重設",
|
||||
"common.archive": "封存",
|
||||
"common.delete": "刪除",
|
||||
|
||||
@@ -53,7 +53,6 @@ import type {
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import { normalize } from "@opencode-ai/session-ui/session-diff"
|
||||
@@ -807,29 +806,6 @@ export function MessageTimeline(props: {
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
|
||||
const exportSession = async (sessionID: string) => {
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
const session = sync().session.get(sessionID)
|
||||
if (!session) return
|
||||
@@ -1588,9 +1564,6 @@ export function MessageTimeline(props: {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => exportSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.export")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -1662,9 +1635,6 @@ export function MessageTimeline(props: {
|
||||
{language.t("session.share.action.share")}...
|
||||
</MenuV2.Item>
|
||||
</Show>
|
||||
<MenuV2.Item onSelect={() => exportSession(id)}>
|
||||
{language.t("common.export")}...
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => void archiveSession(id)}>
|
||||
{language.t("common.archive")}
|
||||
</MenuV2.Item>
|
||||
|
||||
@@ -12,11 +12,10 @@ import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { Message, Part, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionOwnership } from "./session-ownership"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -232,31 +231,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
)
|
||||
}
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openFile = () => {
|
||||
void openDialog(
|
||||
() => import("@/components/dialog-select-file"),
|
||||
@@ -484,14 +458,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: fork,
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.export",
|
||||
title: language.t("command.session.export"),
|
||||
description: language.t("command.session.export.description"),
|
||||
slash: "export",
|
||||
disabled: !params.id,
|
||||
onSelect: exportSession,
|
||||
}),
|
||||
]
|
||||
|
||||
const fileCmds = () => {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fetchSessionExport, sessionExportFilename } from "./session-export"
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
describe("sessionExportFilename", () => {
|
||||
test("generates filename from title", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", title: "Clone PR in worktree from fork" })).toBe(
|
||||
"clone-pr-in-worktree-from-fork.json",
|
||||
)
|
||||
})
|
||||
|
||||
test("generates filename from slug when title missing", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", slug: "my-session-slug" })).toBe("my-session-slug.json")
|
||||
})
|
||||
|
||||
test("falls back to id when title and slug are empty", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123" })).toBe("ses_123.json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchSessionExport", () => {
|
||||
test("fetches full transcript from client", async () => {
|
||||
const session = { id: "ses_1", title: "Test Session" } as Session
|
||||
const msg = { id: "msg_1", role: "user" } as Message
|
||||
const part = { id: "prt_1", type: "text", text: "hello" } as Part
|
||||
const messages = [{ info: msg, parts: [part] }]
|
||||
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: session }),
|
||||
messages: async () => ({ data: messages }),
|
||||
},
|
||||
}
|
||||
|
||||
const result = await fetchSessionExport({
|
||||
sessionID: "ses_1",
|
||||
client,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
info: session,
|
||||
messages,
|
||||
})
|
||||
})
|
||||
|
||||
test("throws when session not found", async () => {
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
fetchSessionExport({
|
||||
sessionID: "ses_missing",
|
||||
client,
|
||||
}),
|
||||
).rejects.toThrow("Session not found: ses_missing")
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
// Matches the exact `{ info, messages: [{ info, parts }] }` structure produced by `opencode export` CLI
|
||||
export type SessionExportData = {
|
||||
info: Session
|
||||
messages: {
|
||||
info: Message
|
||||
parts: Part[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export type SessionExportClient = {
|
||||
session: {
|
||||
get: (input: { sessionID: string }) => Promise<{ data?: Session | null }>
|
||||
messages: (input: { sessionID: string }) => Promise<{ data?: SessionExportData["messages"] | null }>
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSessionExport(input: {
|
||||
sessionID: string
|
||||
client: SessionExportClient
|
||||
}): Promise<SessionExportData> {
|
||||
const [sessionRes, messagesRes] = await Promise.all([
|
||||
input.client.session.get({ sessionID: input.sessionID }),
|
||||
input.client.session.messages({ sessionID: input.sessionID }),
|
||||
])
|
||||
|
||||
if (!sessionRes?.data) {
|
||||
throw new Error(`Session not found: ${input.sessionID}`)
|
||||
}
|
||||
if (!messagesRes?.data) {
|
||||
throw new Error(`Failed to load messages for session: ${input.sessionID}`)
|
||||
}
|
||||
|
||||
return {
|
||||
info: sessionRes.data,
|
||||
messages: messagesRes.data,
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionExportFilename(session: { id: string; title?: string; slug?: string }) {
|
||||
const name = session.title || session.slug || session.id
|
||||
const clean = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/gi, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return `${clean || session.id}.json`
|
||||
}
|
||||
|
||||
export function downloadSessionExport(filename: string, data: unknown) {
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "195K",
|
||||
full: "195,000",
|
||||
compact: "160K",
|
||||
full: "160,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "950",
|
||||
contributors: "900",
|
||||
commits: "13,000",
|
||||
monthlyUsers: "16M",
|
||||
monthlyUsers: "7.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -197,9 +197,7 @@ export async function handler(
|
||||
if (Array.isArray(v)) return [[k, v]]
|
||||
if (typeof v === "object") return [[k, replacer(v)]]
|
||||
if (typeof v === "string") {
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : []
|
||||
if (v === "$org")
|
||||
return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : []
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||
if (v === "$user") return stickyId ? [[k, stickyId]] : []
|
||||
if (v.startsWith("$header.")) {
|
||||
const headerValue = input.request.headers.get(v.slice(8))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -53,6 +53,7 @@ export const BillingTable = mysqlTable(
|
||||
...workspaceIndexes(table),
|
||||
uniqueIndex("global_customer_id").on(table.customerID),
|
||||
uniqueIndex("global_subscription_id").on(table.subscriptionID),
|
||||
uniqueIndex("global_lite_subscription_id").on(table.liteSubscriptionID),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { bigint, index, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
@@ -31,5 +31,8 @@ export const ReferralRewardTable = mysqlTable(
|
||||
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||
timeApplied: utc("time_applied"),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })],
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.workspaceID, table.referralID] }),
|
||||
index("referral_id").on(table.referralID),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -54,15 +54,15 @@ export default {
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
const lakeIngest = getLakeIngest()
|
||||
const [lake] = await Promise.all([
|
||||
// fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// "X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
// },
|
||||
// body: JSON.stringify(events),
|
||||
// }),
|
||||
const [honeycomb, lake] = await Promise.all([
|
||||
fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
}),
|
||||
...(lakeIngest
|
||||
? [
|
||||
fetch(lakeIngest.url, {
|
||||
@@ -76,8 +76,8 @@ export default {
|
||||
]
|
||||
: []),
|
||||
])
|
||||
// console.log(honeycomb.status)
|
||||
// console.log(await honeycomb.text())
|
||||
console.log(honeycomb.status)
|
||||
console.log(await honeycomb.text())
|
||||
if (lake) {
|
||||
console.log(lake.status)
|
||||
console.log(await lake.text())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -5,7 +5,5 @@
|
||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for native menus, picker titles, dialogs, buttons, accessible labels, and displayed errors.
|
||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||
- Keep locale and grammar logic in the shared typed i18n layer. Renderer code should resolve copy through the app language API, and the main process should consume typed native-translation bundles through `nativeT(...)`; native menus, dialogs, and IPC handlers must not inspect locales, choose plural categories, or assemble translated sentence fragments.
|
||||
- Prefer complete translated phrases with only irreducible dynamic placeholders. If native UI needs richer grammar, deepen the shared bundle/API instead of adding locale branches to desktop feature code.
|
||||
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -189,11 +189,6 @@ async function writeZip(output: string, entries: Entry[]) {
|
||||
}
|
||||
|
||||
function initConsoleTransport() {
|
||||
if (app.isPackaged) {
|
||||
log.transports.console.level = false
|
||||
return
|
||||
}
|
||||
|
||||
const write = log.transports.console.writeFn.bind(log.transports.console)
|
||||
log.transports.console.writeFn = (options) => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -21,7 +21,6 @@ await Bun.build({
|
||||
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||
define: {
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
files: {
|
||||
|
||||
@@ -365,7 +365,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
})
|
||||
const footer = shell.footer
|
||||
const rememberLocal = (commit: StreamCommit, after?: LocalReplayAnchor) => {
|
||||
state.localRows = [...state.localRows, { commit, after, created: Date.now() }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
state.localRows = [...state.localRows, { commit, after }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
}
|
||||
|
||||
const loadCatalog = async (): Promise<void> => {
|
||||
|
||||
@@ -266,7 +266,7 @@ export function replayLocalRows(
|
||||
rows: LocalReplayRow[],
|
||||
): StreamCommit[] {
|
||||
const persisted = new Set(messages.map((message) => message.info.id))
|
||||
return rows.reduce((out, local, index) => {
|
||||
return rows.reduce((out, local) => {
|
||||
const row = local.commit
|
||||
if (row.kind === "user" && row.messageID && persisted.has(row.messageID)) {
|
||||
return out
|
||||
@@ -317,35 +317,12 @@ export function replayLocalRows(
|
||||
return [...out.slice(0, after + 1), row, ...out.slice(after + 1)]
|
||||
}
|
||||
|
||||
const nextAnchor = rows
|
||||
.slice(index + 1)
|
||||
.find((next) => next.commit.messageID === row.messageID && next.after)?.after
|
||||
if (nextAnchor) {
|
||||
const before = out.findIndex((commit) =>
|
||||
nextAnchor.partID
|
||||
? commit.partID === nextAnchor.partID
|
||||
: commit.kind === nextAnchor.kind && commit.messageID === nextAnchor.messageID,
|
||||
)
|
||||
if (before !== -1) return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
const before = out.findIndex((commit) => commit.messageID && row.messageID! < commit.messageID)
|
||||
if (before === -1) {
|
||||
return [...out, row]
|
||||
}
|
||||
|
||||
if (local.created !== undefined) {
|
||||
const created = local.created
|
||||
const messageID = row.messageID
|
||||
const later = new Set(
|
||||
messages
|
||||
.filter(
|
||||
(message) =>
|
||||
message.info.time.created > created ||
|
||||
(message.info.time.created === created && message.info.id.localeCompare(messageID) > 0),
|
||||
)
|
||||
.map((message) => message.info.id),
|
||||
)
|
||||
const before = out.findIndex((commit) => commit.messageID && later.has(commit.messageID))
|
||||
if (before !== -1) return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
}
|
||||
|
||||
return [...out, row]
|
||||
return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
}, commits)
|
||||
}
|
||||
|
||||
|
||||
@@ -332,7 +332,6 @@ export type LocalReplayAnchor = {
|
||||
export type LocalReplayRow = {
|
||||
commit: StreamCommit
|
||||
after?: LocalReplayAnchor
|
||||
created?: number
|
||||
}
|
||||
|
||||
// The public contract between the stream transport / prompt queue and
|
||||
|
||||
@@ -9,7 +9,7 @@ let embeddedUIPromise: Promise<Record<string, string> | null> | undefined
|
||||
export const UI_UPSTREAM = new URL("https://app.opencode.ai")
|
||||
|
||||
export const csp = (hash = "") =>
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data:; media-src 'self' data:; connect-src * data: blob:`
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:`
|
||||
export const DEFAULT_CSP = csp()
|
||||
|
||||
export function themePreloadHash(body: string) {
|
||||
|
||||
@@ -49,42 +49,6 @@ type CompletedCompaction = {
|
||||
summary: string | undefined
|
||||
}
|
||||
|
||||
const truncate = (value: string) =>
|
||||
value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]`
|
||||
|
||||
const serialize = (message: SessionV1.WithParts) => {
|
||||
if (message.info.role === "user") {
|
||||
const text = message.parts
|
||||
.filter((part): part is SessionV1.TextPart => part.type === "text" && !part.ignored)
|
||||
.map((part) => part.text)
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
const files = message.parts.flatMap((part) =>
|
||||
part.type === "file" ? [`[Attached ${part.mime}: ${part.filename ?? "file"}]`] : [],
|
||||
)
|
||||
return [...(text ? [`[User]: ${text}`] : []), ...files].join("\n")
|
||||
}
|
||||
return message.parts
|
||||
.flatMap((part) => {
|
||||
if (part.type === "text") return part.text ? [`[Assistant]: ${part.text}`] : []
|
||||
if (part.type === "reasoning") return part.text ? [`[Assistant reasoning]: ${part.text}`] : []
|
||||
if (part.type !== "tool") return []
|
||||
const call = `[Assistant tool call]: ${part.tool}(${JSON.stringify(part.state.input)})`
|
||||
if (part.state.status === "completed") {
|
||||
const attachments = (part.state.attachments ?? []).map(
|
||||
(item) => `[Attached ${item.mime}: ${item.filename ?? "file"}]`,
|
||||
)
|
||||
const output = part.state.time.compacted
|
||||
? "[Old tool result content cleared]"
|
||||
: truncate([part.state.output, ...attachments].join("\n"))
|
||||
return [call, `[Tool result]: ${output}`]
|
||||
}
|
||||
if (part.state.status === "error") return [call, `[Tool error]: ${part.state.error}`]
|
||||
return [call]
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function summaryText(message: SessionV1.WithParts) {
|
||||
const text = message.parts
|
||||
.filter((part): part is SessionV1.TextPart => part.type === "text")
|
||||
@@ -384,7 +348,10 @@ const layer = Layer.effect(
|
||||
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
|
||||
const msgs = structuredClone(selected.head)
|
||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||
const conversation = msgs.map(serialize).filter(Boolean).join("\n\n")
|
||||
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
|
||||
stripMedia: true,
|
||||
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
|
||||
})
|
||||
const ctx = yield* InstanceState.context
|
||||
const msg: SessionV1.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
@@ -425,16 +392,10 @@ const layer = Layer.effect(
|
||||
tools: {},
|
||||
system: [],
|
||||
messages: [
|
||||
...modelMessages,
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [nextPrompt, "The following is the conversation history:", conversation]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text: nextPrompt }],
|
||||
},
|
||||
],
|
||||
model,
|
||||
|
||||
@@ -28,13 +28,11 @@ export const RETRY_BACKOFF_FACTOR = 2
|
||||
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
|
||||
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
|
||||
|
||||
const RETRYABLE_MESSAGE_PATTERNS = [
|
||||
/429|500|502|503|504|524/i,
|
||||
/rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i,
|
||||
/overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i,
|
||||
/terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i,
|
||||
/^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
|
||||
/try your request again|retry your request|resource exhausted|resource_exhausted/i,
|
||||
const RETRYABLE_MESSAGE = [
|
||||
/\b(?:server[_\s-]?error|internal[_\s-]?error|service[_\s-]?unavailable|overloaded|too many requests|rate increased too quickly|provider[_\s-]?returned[_\s-]?error)\b|\brate[_\s-]?limit/i,
|
||||
/\b(?:fetch failed|network error|upstream connect|connection (?:error|refused|lost)|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again)\b|^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed? out)\b/i,
|
||||
/\b(?:resource[_\s-]?exhausted|please retry your request|you can retry your request|try your request again)\b/i,
|
||||
/\b(?:429|500|502|503|504|524)\b/,
|
||||
]
|
||||
|
||||
function cap(ms: number) {
|
||||
@@ -81,13 +79,7 @@ export function retryable(error: Err, provider: string) {
|
||||
const status = error.data.statusCode
|
||||
// 5xx errors are transient server failures and should always be retried,
|
||||
// even when the provider SDK doesn't explicitly mark them as retryable.
|
||||
if (
|
||||
!error.data.isRetryable &&
|
||||
!(status !== undefined && status >= 500) &&
|
||||
!matchesRetryableMessage(error.data.message) &&
|
||||
!matchesRetryableMessage(error.data.responseBody)
|
||||
)
|
||||
return undefined
|
||||
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
|
||||
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
|
||||
return {
|
||||
message: GO_UPSELL_MESSAGE,
|
||||
@@ -142,14 +134,10 @@ export function retryable(error: Err, provider: string) {
|
||||
const lower = message.toLowerCase()
|
||||
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
|
||||
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
|
||||
if (matchesRetryableMessage(message)) return { message }
|
||||
if (RETRYABLE_MESSAGE.some((pattern) => pattern.test(message))) return { message }
|
||||
return undefined
|
||||
}
|
||||
|
||||
function matchesRetryableMessage(value: unknown) {
|
||||
return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value))
|
||||
}
|
||||
|
||||
function str(value: unknown) {
|
||||
if (value === undefined || value === null) return ""
|
||||
return String(value)
|
||||
|
||||
@@ -416,14 +416,7 @@ describe("run session replay", () => {
|
||||
} as const
|
||||
|
||||
expect(
|
||||
replayLocalRows(
|
||||
[userMessage("msg-user-2", "successful")],
|
||||
[persisted],
|
||||
[
|
||||
{ commit: failed, created: 0 },
|
||||
{ commit: error, created: 0 },
|
||||
],
|
||||
),
|
||||
replayLocalRows([userMessage("msg-user-2", "successful")], [persisted], [{ commit: failed }, { commit: error }]),
|
||||
).toEqual([failed, error, persisted])
|
||||
})
|
||||
|
||||
@@ -664,35 +657,35 @@ describe("run session replay", () => {
|
||||
).toEqual([prompt, running, error, completed])
|
||||
})
|
||||
|
||||
test("appends an unanchored local diagnostic without inferring chronology from message IDs", () => {
|
||||
test("retains an unpersisted local diagnostic before later persisted prompts", () => {
|
||||
const first = {
|
||||
kind: "user",
|
||||
text: "before",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-z",
|
||||
messageID: "msg-user-1",
|
||||
} as const
|
||||
const error = {
|
||||
kind: "error",
|
||||
text: "failed to start new session",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-m",
|
||||
messageID: "msg-user-2",
|
||||
} as const
|
||||
const second = {
|
||||
kind: "user",
|
||||
text: "after",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-a",
|
||||
messageID: "msg-user-3",
|
||||
} as const
|
||||
|
||||
expect(
|
||||
replayLocalRows(
|
||||
[userMessage("msg-user-z", "before"), userMessage("msg-user-a", "after")],
|
||||
[userMessage("msg-user-1", "before"), userMessage("msg-user-3", "after")],
|
||||
[first, second],
|
||||
[{ commit: error }],
|
||||
),
|
||||
).toEqual([first, second, error])
|
||||
).toEqual([first, error, second])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -326,7 +326,7 @@ describe("HttpApi UI fallback", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("allows embedded UI terminal wasm, blob attachments, and theme preload CSP", () =>
|
||||
it.live("allows embedded UI terminal wasm and theme preload CSP", () =>
|
||||
Effect.gen(function* () {
|
||||
const script = 'document.documentElement.dataset.theme = "dark"'
|
||||
|
||||
@@ -351,8 +351,7 @@ describe("HttpApi UI fallback", () => {
|
||||
const csp = response.headers.get("content-security-policy") ?? ""
|
||||
expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'")
|
||||
expect(csp).toContain(`'sha256-${createHash("sha256").update(script).digest("base64")}'`)
|
||||
expect(csp).toContain("img-src 'self' data: https: blob:")
|
||||
expect(csp).toContain("connect-src * data: blob:")
|
||||
expect(csp).toContain("connect-src * data:")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1362,10 +1362,10 @@ describe("session.compaction.process", () => {
|
||||
"summarizes only the head while keeping recent tail out of summary input",
|
||||
() => {
|
||||
const stub = llm()
|
||||
let messages: LLM.StreamInput["messages"] = []
|
||||
let captured = ""
|
||||
stub.push(
|
||||
reply("summary", (input) => {
|
||||
messages = input.messages
|
||||
captured = JSON.stringify(input.messages)
|
||||
}),
|
||||
)
|
||||
return Effect.gen(function* () {
|
||||
@@ -1386,10 +1386,7 @@ describe("session.compaction.process", () => {
|
||||
auto: false,
|
||||
})
|
||||
|
||||
const captured = JSON.stringify(messages)
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0]?.role).toBe("user")
|
||||
expect(captured).toContain("[User]: older context")
|
||||
expect(captured).toContain("older context")
|
||||
expect(captured).not.toContain("keep this turn")
|
||||
expect(captured).not.toContain("and this one too")
|
||||
expect(captured).not.toContain("What did we do so far?")
|
||||
@@ -1440,74 +1437,6 @@ describe("session.compaction.process", () => {
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
itCompaction.instance(
|
||||
"serializes repeated compaction history as one user message",
|
||||
() => {
|
||||
const stub = llm()
|
||||
let captured: LLM.StreamInput["messages"] = []
|
||||
stub.push(
|
||||
reply("summary two", (input) => {
|
||||
captured = input.messages
|
||||
}),
|
||||
)
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const ssn = yield* SessionNs.Service
|
||||
const test = yield* TestInstance
|
||||
const session = yield* ssn.create({})
|
||||
const turn = yield* createUserMessage(session.id, "original request")
|
||||
const kept = yield* createAssistantMessage(session.id, turn.id, test.directory)
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: kept.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: "read-call",
|
||||
tool: "read",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { filePath: "src/index.ts" },
|
||||
output: "file contents",
|
||||
title: "src/index.ts",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
|
||||
const previous = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
model: ref,
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: previous.id,
|
||||
sessionID: session.id,
|
||||
type: "compaction",
|
||||
auto: false,
|
||||
tail_start_id: kept.id,
|
||||
})
|
||||
yield* createSummaryAssistantMessage(session.id, previous.id, test.directory, "summary one")
|
||||
yield* createCompactionMarker(session.id)
|
||||
|
||||
const msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]?.role).toBe("user")
|
||||
expect(JSON.stringify(captured)).toContain('[Assistant tool call]: read({\\"filePath\\":\\"src/index.ts\\"})')
|
||||
expect(JSON.stringify(captured)).toContain("[Tool result]: file contents")
|
||||
expect(JSON.stringify(captured)).not.toContain('\\"role\\":\\"assistant\\"')
|
||||
}).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 0 }) }))
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
||||
const stub = llm()
|
||||
stub.push(reply("summary one"))
|
||||
|
||||
@@ -604,53 +604,6 @@ it.live("session.processor effect tests retry recognized structured json errors"
|
||||
),
|
||||
)
|
||||
|
||||
it.live("session.processor effect tests retry OpenAI-compatible midstream server errors", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.push(raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }))
|
||||
yield* llm.text("after")
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "retry midstream server error")
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const handle = yield* processors.create({
|
||||
assistantMessage: msg,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
})
|
||||
|
||||
const value = yield* handle.process({
|
||||
user: {
|
||||
id: parent.id,
|
||||
sessionID: chat.id,
|
||||
role: "user",
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies SessionV1.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "retry midstream server error" }],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
|
||||
expect(handle.message.error).toBeUndefined()
|
||||
}),
|
||||
{ config: (url) => providerCfg(url) },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("session.processor effect tests publish retry status updates", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
|
||||
@@ -170,43 +170,17 @@ describe("session.retry.retryable", () => {
|
||||
|
||||
test.each([
|
||||
"Internal server error",
|
||||
"internal error",
|
||||
"server-error",
|
||||
"Provider returned error",
|
||||
"provider-returned-error",
|
||||
"terminated",
|
||||
"fetch failed",
|
||||
"connection refused",
|
||||
"connect ECONNREFUSED",
|
||||
"request ETIMEDOUT",
|
||||
"failed to fetch",
|
||||
"EAI_AGAIN",
|
||||
"response timed out",
|
||||
"Please retry your request",
|
||||
"try your request again",
|
||||
"upstream returned status 524",
|
||||
])("retries matching API error text: %s", (message) => {
|
||||
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
|
||||
})
|
||||
|
||||
test("retries hyphenated service-unavailable errors", () => {
|
||||
expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({
|
||||
message: "Provider is overloaded",
|
||||
})
|
||||
})
|
||||
|
||||
test("matches retryable API response bodies", () => {
|
||||
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
||||
new SessionV1.APIError({
|
||||
message: "Request failed",
|
||||
isRetryable: false,
|
||||
statusCode: 400,
|
||||
responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }),
|
||||
}).toObject(),
|
||||
)
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" })
|
||||
})
|
||||
|
||||
test("retries transport timeout errors", () => {
|
||||
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
|
||||
expect(SessionV1.APIError.isInstance(request)).toBe(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
githubLink,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
@@ -241,11 +240,7 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp
|
||||
<Meta name="twitter:description" content={description()} />
|
||||
<script type="application/ld+json">{structuredData()}</script>
|
||||
</Show>
|
||||
<Header
|
||||
githubStars={githubStars() ?? githubLink.fallbackStars}
|
||||
links={compareHeaderLinks}
|
||||
brandHref={import.meta.env.BASE_URL}
|
||||
/>
|
||||
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<ComparisonHero
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @refresh reload
|
||||
import type { Asset, PageEvent } from "@solidjs/start"
|
||||
import { createHandler, StartServer } from "@solidjs/start/server"
|
||||
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { dir, localeFromRequest, tag } from "./lib/language"
|
||||
import statsStylesheetUrl from "./routes/index.css?url"
|
||||
|
||||
const statsThemePreloadScript = `;(function () {
|
||||
var preference = "system"
|
||||
@@ -21,12 +18,8 @@ export default createHandler(
|
||||
() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => {
|
||||
const event = getRequestEvent() as PageEvent | undefined
|
||||
const event = getRequestEvent()
|
||||
const locale = event ? localeFromRequest(event.request) : "en"
|
||||
const stylesheet = (event?.assets as Asset[] | undefined)?.find(
|
||||
(asset): asset is Extract<Asset, { tag: "link" }> => asset.tag === "link" && asset.attrs.rel === "stylesheet",
|
||||
)
|
||||
const stylesheetHref = import.meta.env.DEV ? statsStylesheetUrl : stylesheet?.attrs.href
|
||||
|
||||
return (
|
||||
<html lang={tag(locale)} dir={dir(locale)} data-locale={locale}>
|
||||
@@ -34,8 +27,6 @@ export default createHandler(
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script id="stats-theme-preload-script">{statsThemePreloadScript}</script>
|
||||
<link rel="preload" href={ibmPlexMonoMediumLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
{stylesheetHref ? <link rel="stylesheet" href={stylesheetHref} /> : null}
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { scaleSqrt } from "d3-scale"
|
||||
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
|
||||
import {
|
||||
getStatsModelData,
|
||||
type CountryEntry,
|
||||
type ModelPeerEntry,
|
||||
type ModelUsagePoint,
|
||||
type StatsModelData,
|
||||
type UsageRange,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
import { LocaleLinks } from "../../component/locale-links"
|
||||
import { useI18n } from "../../context/i18n"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { localizedUrl } from "../../lib/language"
|
||||
import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog"
|
||||
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map"
|
||||
import {
|
||||
findModelCatalogEntry,
|
||||
formatCatalogLabName,
|
||||
getModelCatalog,
|
||||
type ModelCatalog,
|
||||
type ModelCatalogEntry,
|
||||
} from "../model-catalog"
|
||||
import { SectionHeading } from "../section-heading"
|
||||
import { runStatsEffect } from "../../stats-runtime"
|
||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||
@@ -32,7 +43,6 @@ import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
githubLink,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
@@ -41,41 +51,45 @@ import {
|
||||
} from "../stats-shell"
|
||||
|
||||
const statsUnfurlPath = "banner.png"
|
||||
const geoMapWidth = 960
|
||||
const geoMapHeight = 430
|
||||
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
type IsoCountryCode = readonly [string, string, string]
|
||||
type ModelCatalogOption = Pick<ModelCatalogEntry, "id" | "lab" | "slug" | "name">
|
||||
type ModelPageCatalog = {
|
||||
entry: ModelCatalogEntry | null
|
||||
labs: { id: string; name: string }[]
|
||||
labModels: ModelCatalogOption[]
|
||||
}
|
||||
type StatsModelPageData = Omit<StatsModelData, "country"> & { country: CountryEntry[] }
|
||||
type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null }
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
|
||||
const countryNumericIds = new Map(
|
||||
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
|
||||
)
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
marker: geoCountryMarker(country),
|
||||
}))
|
||||
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
const getModelPageData = query(async (labParam: string, modelParam: string) => {
|
||||
const getModelData = query(async (lab: string, model: string) => {
|
||||
"use server"
|
||||
const catalog = await loadModelCatalog()
|
||||
const entry = findModelCatalogEntry(catalog, modelParam, labParam) ?? null
|
||||
const lab = entry?.lab ?? labParam
|
||||
const model = entry?.slug ?? modelParam
|
||||
const stats = lab && model ? await runStatsEffect(getStatsModelData(model, lab)) : null
|
||||
return {
|
||||
catalog: {
|
||||
entry,
|
||||
labs: catalog.labs.map((item) => ({ id: item.id, name: item.name })),
|
||||
labModels:
|
||||
catalog.labs
|
||||
.find((item) => item.id === (entry?.lab ?? providerSlug(labParam)))
|
||||
?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [],
|
||||
},
|
||||
stats: stats ? { ...stats, country: stats.country["2M"] } : null,
|
||||
} satisfies ModelPageData
|
||||
}, "getStatsModelPageData")
|
||||
return runStatsEffect(getStatsModelData(model, lab))
|
||||
}, "getStatsModelData")
|
||||
|
||||
export default function StatsModel() {
|
||||
const i18n = useI18n()
|
||||
@@ -85,9 +99,18 @@ export default function StatsModel() {
|
||||
const params = useParams()
|
||||
const labParam = createMemo(() => params.lab ?? "")
|
||||
const modelParam = createMemo(() => params.model ?? "")
|
||||
const page = createAsync(() => getModelPageData(labParam(), modelParam()))
|
||||
const catalogEntry = createMemo(() => page()?.catalog.entry)
|
||||
const stats = createMemo(() => page()?.stats)
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const catalogEntry = createMemo(() => {
|
||||
const data = catalog()
|
||||
if (!data) return undefined
|
||||
return findModelCatalogEntry(data, modelParam(), labParam()) ?? null
|
||||
})
|
||||
const stats = createAsync(() => {
|
||||
const entry = catalogEntry()
|
||||
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
|
||||
if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null)
|
||||
return getModelData(labParam(), entry?.slug ?? modelParam())
|
||||
})
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback"))
|
||||
@@ -153,20 +176,16 @@ export default function StatsModel() {
|
||||
<Meta name="twitter:description" content={modelDescription()} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Header
|
||||
githubStars={githubStars() ?? githubLink.fallbackStars}
|
||||
links={modelHeaderLinks()}
|
||||
brandHref={import.meta.env.BASE_URL}
|
||||
/>
|
||||
<Header githubStars={githubStars() ?? "150K"} links={modelHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<Show when={page() !== undefined} fallback={<ModelLoading />}>
|
||||
<Show when={catalogEntry() || stats() !== undefined} fallback={<ModelLoading />}>
|
||||
<Show when={catalogEntry() || stats()} fallback={<ModelNotFound lab={labParam()} model={modelParam()} />}>
|
||||
<>
|
||||
<ModelHero
|
||||
data={stats() ?? null}
|
||||
catalog={catalogEntry() ?? null}
|
||||
catalogData={page()?.catalog ?? null}
|
||||
catalogData={catalog() ?? null}
|
||||
labName={labName()}
|
||||
/>
|
||||
<ModelOverview catalog={catalogEntry() ?? null} />
|
||||
@@ -174,10 +193,10 @@ export default function StatsModel() {
|
||||
<ModelUsageSection data={stats() ?? null} />
|
||||
<ModelUniqueUsersSection data={stats() ?? null} />
|
||||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? []} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||
<ModelPeersSection data={stats() ?? null} />
|
||||
<ComparisonCardsSection
|
||||
pairs={modelComparisonPairs(page()?.catalog.labModels, catalogEntry() ?? null, stats() ?? null)}
|
||||
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
|
||||
title="Compare This Model"
|
||||
description="Other models to compare with this one."
|
||||
variant="featured"
|
||||
@@ -252,9 +271,9 @@ function ModelNotFound(props: { lab: string; model: string }) {
|
||||
}
|
||||
|
||||
function ModelHero(props: {
|
||||
data: StatsModelPageData | null
|
||||
data: StatsModelData | null
|
||||
catalog: ModelCatalogEntry | null
|
||||
catalogData: ModelPageCatalog | null
|
||||
catalogData: ModelCatalog | null
|
||||
labName: string
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
@@ -263,7 +282,9 @@ function ModelHero(props: {
|
||||
const modelName = () => props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")
|
||||
const weights = () => props.catalog?.weights[0]
|
||||
const labs = () => props.catalogData?.labs ?? []
|
||||
const labModels = () => props.catalogData?.labModels ?? (props.catalog ? [props.catalog] : [])
|
||||
const labModels = () =>
|
||||
props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
|
||||
(props.catalog ? [props.catalog] : [])
|
||||
return (
|
||||
<section id="overview" data-section="model-hero">
|
||||
<nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
|
||||
@@ -382,7 +403,7 @@ function ModelHeroActionIcon(props: { kind: "weights" | "compare" }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelHeroSparkline(props: { data: StatsModelPageData }) {
|
||||
function ModelHeroSparkline(props: { data: StatsModelData }) {
|
||||
const values = () => props.data.usage.slice(-14).map((point) => point.tokens)
|
||||
return (
|
||||
<span data-slot="model-hero-sparkline" aria-hidden="true">
|
||||
@@ -445,7 +466,7 @@ function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
|
||||
function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
@@ -484,7 +505,7 @@ function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MomentumChart(props: { data: StatsModelPageData; locale: string }) {
|
||||
function MomentumChart(props: { data: StatsModelData; locale: string }) {
|
||||
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
|
||||
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
|
||||
return (
|
||||
@@ -541,7 +562,7 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUsageSection(props: { data: StatsModelPageData | null }) {
|
||||
function ModelUsageSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
@@ -561,7 +582,7 @@ function ModelUsageSection(props: { data: StatsModelPageData | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
|
||||
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
@@ -585,7 +606,7 @@ function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
|
||||
}
|
||||
|
||||
function ModelTrendSection(props: {
|
||||
data: StatsModelPageData | null
|
||||
data: StatsModelData | null
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
@@ -838,7 +859,7 @@ function ModelTrendSection(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalog: ModelCatalogEntry | null }) {
|
||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<section id="efficiency" data-section="model-panel">
|
||||
@@ -889,11 +910,11 @@ function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalo
|
||||
)
|
||||
}
|
||||
|
||||
function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
|
||||
function ModelGeoBreakdownSection(props: { data: Record<UsageRange, CountryEntry[]> }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const [activeCountry, setActiveCountry] = createSignal<string>()
|
||||
const data = createMemo(() => props.data)
|
||||
const data = createMemo(() => props.data["2M"])
|
||||
const countryById = createMemo(
|
||||
() =>
|
||||
new Map(
|
||||
@@ -1010,30 +1031,31 @@ function GeoWorldMap(props: {
|
||||
</For>
|
||||
</g>
|
||||
<g data-slot="geo-country-markers">
|
||||
<For each={worldCountryMarkers}>
|
||||
<For each={worldCountryPaths}>
|
||||
{(country) => {
|
||||
const entry = () => props.countryById.get(country.id)
|
||||
return (
|
||||
<Show when={entry()}>
|
||||
<circle
|
||||
cx={country.marker.x}
|
||||
cy={country.marker.y}
|
||||
data-country-id={country.id}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
<Show when={country.marker && entry() ? country.marker : undefined}>
|
||||
{(marker) => (
|
||||
<circle
|
||||
cx={marker().x}
|
||||
cy={marker().y}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
@@ -1081,7 +1103,7 @@ function GeoCountryList(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelPeersSection(props: { data: StatsModelPageData | null }) {
|
||||
function ModelPeersSection(props: { data: StatsModelData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<section id="peers" data-section="model-panel">
|
||||
@@ -1150,9 +1172,9 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
|
||||
}
|
||||
|
||||
function modelComparisonPairs(
|
||||
catalogModels: ModelCatalogOption[] | undefined,
|
||||
catalog: ModelCatalog | undefined,
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelPageData | null,
|
||||
data: StatsModelData | null,
|
||||
) {
|
||||
const current = modelComparisonRef(catalogEntry, data)
|
||||
if (!current) return []
|
||||
@@ -1170,7 +1192,9 @@ function modelComparisonPairs(
|
||||
},
|
||||
detail: "Usage peer",
|
||||
}))
|
||||
const catalogPairs = (catalogEntry ? (catalogModels ?? []) : [])
|
||||
const catalogPairs = (
|
||||
catalogEntry && catalog ? (catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? []) : []
|
||||
)
|
||||
.filter((model) => model.id !== catalogEntry?.id)
|
||||
.slice(0, 3)
|
||||
.map((model) => ({
|
||||
@@ -1183,7 +1207,7 @@ function modelComparisonPairs(
|
||||
|
||||
function modelComparisonRef(
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelPageData | null,
|
||||
data: StatsModelData | null,
|
||||
): ComparisonModelRef | undefined {
|
||||
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
|
||||
if (!data) return undefined
|
||||
@@ -1203,10 +1227,31 @@ function getProviderIconId(author: string) {
|
||||
return author.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
||||
}
|
||||
|
||||
function emptyCountryRecord(): Record<UsageRange, CountryEntry[]> {
|
||||
return {
|
||||
"1D": [],
|
||||
"1W": [],
|
||||
"2W": [],
|
||||
"1M": [],
|
||||
"2M": [],
|
||||
"3M": [],
|
||||
YTD: [],
|
||||
ALL: [],
|
||||
}
|
||||
}
|
||||
|
||||
function countryNumericId(country: string) {
|
||||
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
|
||||
}
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function formatCountryName(country: string, locale: string, i18n: ReturnType<typeof useI18n>) {
|
||||
const code = country.toUpperCase()
|
||||
if (code === "ZZ") return i18n.t("home.unknown")
|
||||
@@ -1463,7 +1508,7 @@ function formatSparklinePoint(value: number) {
|
||||
return Number(value.toFixed(2)).toString()
|
||||
}
|
||||
|
||||
function formatModelRankMoveLabel(data: StatsModelPageData, i18n: ReturnType<typeof useI18n>) {
|
||||
function formatModelRankMoveLabel(data: StatsModelData, i18n: ReturnType<typeof useI18n>) {
|
||||
if (data.rank === null) return i18n.t("model.noUsageLastWeek")
|
||||
if (data.previousRank === null) return i18n.t("model.newThisWeek")
|
||||
const change = data.previousRank - data.rank
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type LabUsageModelEntry,
|
||||
type MarketDay,
|
||||
type ModelUsagePoint,
|
||||
type StatsHomeData,
|
||||
type StatsLabData,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
catalogSlug,
|
||||
findModelCatalogLab,
|
||||
formatCatalogLabName,
|
||||
loadModelCatalog,
|
||||
getModelCatalog,
|
||||
type ModelCatalogEntry,
|
||||
type ModelCatalogLab,
|
||||
} from "../model-catalog"
|
||||
@@ -32,7 +33,6 @@ import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
githubLink,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
@@ -42,33 +42,15 @@ import {
|
||||
|
||||
const statsUnfurlPath = "banner.png"
|
||||
|
||||
type RelatedCatalogLab = Pick<ModelCatalogLab, "id" | "name" | "description"> & {
|
||||
models: Pick<ModelCatalogEntry, "name">[]
|
||||
}
|
||||
|
||||
type LabPageData = {
|
||||
lab: ModelCatalogLab | null
|
||||
labs: RelatedCatalogLab[]
|
||||
market: MarketDay[]
|
||||
stats: StatsLabData | null
|
||||
}
|
||||
|
||||
const getLabPageData = query(async (labParam: string) => {
|
||||
const getLabData = query(async (lab: string) => {
|
||||
"use server"
|
||||
const [catalog, home] = await Promise.all([loadModelCatalog(), runStatsEffect(getStatsHomeData())])
|
||||
const lab = findModelCatalogLab(catalog, labParam) ?? null
|
||||
return {
|
||||
lab,
|
||||
labs: catalog.labs.map((entry) => ({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
models: entry.models.map((model) => ({ name: model.name })),
|
||||
})),
|
||||
market: home.market["2M"],
|
||||
stats: lab ? await runStatsEffect(getStatsLabData(lab.id)) : null,
|
||||
} satisfies LabPageData
|
||||
}, "getStatsLabPageData")
|
||||
return runStatsEffect(getStatsLabData(lab))
|
||||
}, "getStatsLabData")
|
||||
|
||||
const getHomeData = query(async () => {
|
||||
"use server"
|
||||
return runStatsEffect(getStatsHomeData())
|
||||
}, "getStatsHomeData")
|
||||
|
||||
type LabModelTooltipState = {
|
||||
model: ModelCatalogEntry
|
||||
@@ -85,9 +67,19 @@ export default function StatsLab() {
|
||||
setStatsPageCacheHeaders(event?.response.headers)
|
||||
const params = useParams()
|
||||
const labParam = createMemo(() => params.lab ?? "")
|
||||
const page = createAsync(() => getLabPageData(labParam()))
|
||||
const lab = createMemo(() => page()?.lab)
|
||||
const stats = createMemo(() => page()?.stats)
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const lab = createMemo(() => {
|
||||
const data = catalog()
|
||||
if (!data) return undefined
|
||||
return findModelCatalogLab(data, labParam()) ?? null
|
||||
})
|
||||
const stats = createAsync(() => {
|
||||
const entry = lab()
|
||||
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
|
||||
if (!entry) return Promise.resolve(null)
|
||||
return getLabData(entry.id)
|
||||
})
|
||||
const homeStats = createAsync((): Promise<StatsHomeData | undefined> => getHomeData())
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam()))
|
||||
@@ -142,22 +134,22 @@ export default function StatsLab() {
|
||||
<Meta name="twitter:description" content={labDescription()} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Header
|
||||
githubStars={githubStars() ?? githubLink.fallbackStars}
|
||||
links={labHeaderLinks()}
|
||||
brandHref={import.meta.env.BASE_URL}
|
||||
/>
|
||||
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<Show when={page() !== undefined} fallback={<LabLoading />}>
|
||||
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={page()?.labs ?? []} />}>
|
||||
<Show when={catalog() !== undefined} fallback={<LabLoading />}>
|
||||
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={catalog()?.labs ?? []} />}>
|
||||
{(data) => (
|
||||
<>
|
||||
<LabHero lab={data()} labs={page()?.labs ?? []} />
|
||||
<LabHero lab={data()} labs={catalog()?.labs ?? []} />
|
||||
<LabOverview lab={data()} data={stats() ?? null} />
|
||||
<LabUsageSection lab={data()} data={stats() ?? null} />
|
||||
<LabModelsSection lab={data()} usage={stats()?.models ?? []} />
|
||||
<LabRelatedSection lab={data()} labs={page()?.labs ?? []} market={page()?.market ?? []} />
|
||||
<LabRelatedSection
|
||||
lab={data()}
|
||||
labs={catalog()?.labs ?? []}
|
||||
market={homeStats()?.market["2M"] ?? []}
|
||||
/>
|
||||
<ComparisonCardsSection
|
||||
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
|
||||
title={`${data().name} Model Comparisons`}
|
||||
@@ -190,7 +182,7 @@ function LabLoading() {
|
||||
)
|
||||
}
|
||||
|
||||
function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
|
||||
function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
|
||||
const i18n = useI18n()
|
||||
const labName = () => formatCatalogLabName(props.lab)
|
||||
return (
|
||||
@@ -202,7 +194,7 @@ function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
|
||||
function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
|
||||
return (
|
||||
<section id="overview" data-section="lab-hero">
|
||||
<LabHeroBreadcrumb label={props.lab.name} labs={props.labs} />
|
||||
@@ -211,7 +203,7 @@ function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabHeroBreadcrumb(props: { label: string; labs?: RelatedCatalogLab[] }) {
|
||||
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
|
||||
const language = useLanguage()
|
||||
const labs = () => props.labs ?? []
|
||||
const current = () => labs().find((lab) => lab.name === props.label)
|
||||
@@ -676,7 +668,7 @@ function LabModelTooltip(props: { state: LabModelTooltipState }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[]; market: MarketDay[] }) {
|
||||
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[]; market: MarketDay[] }) {
|
||||
const related = createMemo(() => relatedLabs(props.lab, props.labs, props.market))
|
||||
return (
|
||||
<section id="related-labs" data-section="model-panel" data-variant="lab-related">
|
||||
@@ -765,9 +757,9 @@ function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
|
||||
)
|
||||
}
|
||||
|
||||
type RelatedLabEntry = { lab: RelatedCatalogLab; share: number; tokens: number }
|
||||
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
|
||||
|
||||
function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
|
||||
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
|
||||
const stats = relatedLabStats(labs, market)
|
||||
return labs
|
||||
.filter((lab) => lab.id !== current.id)
|
||||
@@ -776,8 +768,8 @@ function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market
|
||||
.slice(0, 3)
|
||||
}
|
||||
|
||||
function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
|
||||
const labByKey = new Map<string, RelatedCatalogLab>()
|
||||
function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
|
||||
const labByKey = new Map<string, ModelCatalogLab>()
|
||||
labs.forEach((lab) => {
|
||||
labByKey.set(lab.id, lab)
|
||||
labByKey.set(catalogSlug(lab.name), lab)
|
||||
@@ -802,7 +794,7 @@ function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
|
||||
)
|
||||
}
|
||||
|
||||
function labRelatedDescription(lab: RelatedCatalogLab) {
|
||||
function labRelatedDescription(lab: ModelCatalogLab) {
|
||||
return lab.description ?? ""
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export type ComparisonPair = {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function modelRefFromCatalog(entry: Pick<ModelCatalogEntry, "name" | "lab" | "slug">): ComparisonModelRef {
|
||||
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
|
||||
return {
|
||||
name: entry.name,
|
||||
lab: entry.lab,
|
||||
@@ -83,7 +83,11 @@ export function ComparisonCardsSection(props: {
|
||||
|
||||
function FeaturedComparisonCard(props: { pair: ComparisonPair }) {
|
||||
return (
|
||||
<a data-component="compare-home-card" href={canonicalComparisonHref(props.pair.first, props.pair.second)}>
|
||||
<a
|
||||
data-component="compare-home-card"
|
||||
href={canonicalComparisonHref(props.pair.first, props.pair.second)}
|
||||
aria-label={`${props.pair.detail}: ${props.pair.first.name} vs ${props.pair.second.name}`}
|
||||
>
|
||||
<span data-slot="compare-home-card-head">
|
||||
<span>
|
||||
<strong>{props.pair.detail}</strong>
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
applyThemePreference,
|
||||
Footer,
|
||||
getGitHubStars,
|
||||
githubLink,
|
||||
Header,
|
||||
isThemePreference,
|
||||
themeStorageKey,
|
||||
@@ -126,11 +125,7 @@ export default function ModelCompareIndex() {
|
||||
<Meta name="twitter:description" content={compareDescription} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Header
|
||||
githubStars={githubStars() ?? githubLink.fallbackStars}
|
||||
links={compareHeaderLinks()}
|
||||
brandHref={import.meta.env.BASE_URL}
|
||||
/>
|
||||
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<section id="compare-tool" data-section="compare-home-hero">
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-110m.json?raw"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
|
||||
export const geoMapWidth = 960
|
||||
export const geoMapHeight = 430
|
||||
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
|
||||
export const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
}))
|
||||
|
||||
export const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
// The 110m topology omits small regions. Geographic centroids keep those countries interactive without shipping 50m paths.
|
||||
const fallbackCountryMarkerCoordinates = [
|
||||
["016", -170.7179, -14.3046],
|
||||
["020", 1.5606, 42.542],
|
||||
["028", -61.7945, 17.2762],
|
||||
["048", 50.5425, 26.0417],
|
||||
["052", -59.5602, 13.1811],
|
||||
["060", -64.7558, 32.3131],
|
||||
["086", 72.4453, -7.3312],
|
||||
["092", -64.4704, 18.5276],
|
||||
["132", -23.9576, 15.9551],
|
||||
["136", -80.9129, 19.43],
|
||||
["174", 43.6844, -11.879],
|
||||
["184", -159.7871, -21.2195],
|
||||
["212", -61.3576, 15.4394],
|
||||
["234", -6.8808, 62.0527],
|
||||
["239", -36.4863, -54.4641],
|
||||
["248", 19.9528, 60.2153],
|
||||
["258", -144.8045, -14.7283],
|
||||
["296", -167.9217, 0.893],
|
||||
["308", -61.6818, 12.1174],
|
||||
["316", 144.767, 13.4406],
|
||||
["334", 73.52, -53.0872],
|
||||
["336", 12.4343, 41.9021],
|
||||
["344", 114.1143, 22.3983],
|
||||
["438", 9.5357, 47.1367],
|
||||
["446", 113.509, 22.2231],
|
||||
["462", 73.4573, 3.7316],
|
||||
["470", 14.405, 35.9215],
|
||||
["480", 57.5714, -20.2779],
|
||||
["492", 7.4073, 43.7526],
|
||||
["500", -62.1856, 16.7404],
|
||||
["520", 166.9326, -0.5189],
|
||||
["531", -68.9721, 12.1957],
|
||||
["533", -69.9827, 12.521],
|
||||
["534", -63.0572, 18.0509],
|
||||
["570", -169.8704, -19.0489],
|
||||
["574", 167.9497, -29.0516],
|
||||
["580", 145.6193, 15.8288],
|
||||
["583", 153.2966, 7.5361],
|
||||
["584", 170.3313, 7.015],
|
||||
["585", 134.4056, 7.286],
|
||||
["612", -128.3167, -24.3649],
|
||||
["652", -62.841, 17.8988],
|
||||
["654", -9.7009, -12.3548],
|
||||
["659", -62.6873, 17.2647],
|
||||
["660", -63.066, 18.2243],
|
||||
["662", -60.9696, 13.8946],
|
||||
["663", -63.0599, 18.0888],
|
||||
["666", -56.3037, 46.9187],
|
||||
["670", -61.2008, 13.2251],
|
||||
["674", 12.4594, 43.9415],
|
||||
["678", 6.7235, 0.4434],
|
||||
["690", 55.476, -4.6601],
|
||||
["702", 103.817, 1.359],
|
||||
["776", -174.7998, -20.4161],
|
||||
["796", -71.9734, 21.8312],
|
||||
["831", -2.5726, 49.4678],
|
||||
["832", -2.1272, 49.2181],
|
||||
["833", -4.5388, 54.224],
|
||||
["850", -64.8028, 17.9555],
|
||||
["876", -177.3469, -13.8898],
|
||||
["882", -172.1649, -13.7536],
|
||||
] as const
|
||||
|
||||
export const worldCountryMarkers = [
|
||||
...worldCountries.features.flatMap((country) => {
|
||||
const marker = geoCountryMarker(country)
|
||||
return marker ? [{ id: String(country.id ?? "").padStart(3, "0"), marker }] : []
|
||||
}),
|
||||
...fallbackCountryMarkerCoordinates.flatMap(([id, longitude, latitude]) => {
|
||||
const marker = worldProjection([longitude, latitude])
|
||||
return marker ? [{ id, marker: { x: marker[0], y: marker[1] } }] : []
|
||||
}),
|
||||
]
|
||||
@@ -55,7 +55,7 @@ body {
|
||||
--color-background-strong: #161616;
|
||||
--color-background-strong-hover: #242424;
|
||||
--color-text: #5c5c5c;
|
||||
--color-text-weak: #707070;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-strong: #161616;
|
||||
--color-text-inverted: #ffffff;
|
||||
--color-border-weak: #0000001a;
|
||||
@@ -66,10 +66,10 @@ body {
|
||||
--stats-line-strong: #00000033;
|
||||
--stats-text: #161616;
|
||||
--stats-muted: #5c5c5c;
|
||||
--stats-faint: #707070;
|
||||
--stats-faint: #808080;
|
||||
--stats-theme-icon-active: #3a3a3a;
|
||||
--stats-accent: #3b5cf6;
|
||||
--stats-accent-text: #3b5cf6;
|
||||
--stats-accent-text: #6c7dff;
|
||||
--stats-bar-idle: #d4d4d4;
|
||||
--stats-dot: #d4d4d4;
|
||||
--stats-hero-muted: #5c5c5c;
|
||||
@@ -92,18 +92,6 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="visually-hidden"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"][data-layout="compare-detail"] {
|
||||
/* The table contains its own wide rows; keep the page itself out of the horizontal scroll chain. */
|
||||
overflow-x: visible;
|
||||
@@ -7091,7 +7079,7 @@ body {
|
||||
--color-background-strong: #ffffff;
|
||||
--color-background-strong-hover: #eeeeee;
|
||||
--color-text: #d4d4d4;
|
||||
--color-text-weak: #a3a3a3;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-strong: #ffffff;
|
||||
--color-text-inverted: #161616;
|
||||
--color-border-weak: #ffffff1a;
|
||||
@@ -7102,12 +7090,11 @@ body {
|
||||
--stats-line-strong: #ffffff33;
|
||||
--stats-text: #ffffff;
|
||||
--stats-muted: #d4d4d4;
|
||||
--stats-faint: #a3a3a3;
|
||||
--stats-accent-text: #8190ff;
|
||||
--stats-faint: #808080;
|
||||
--stats-theme-icon-active: #fafafa;
|
||||
--stats-bar-idle: #303030;
|
||||
--stats-dot: #303030;
|
||||
--stats-hero-muted: #a3a3a3;
|
||||
--stats-hero-muted: #808080;
|
||||
--stats-hero-pattern: #303030;
|
||||
--stats-logo-bg: #f1ecec;
|
||||
--stats-logo-fill: #b7b1b1;
|
||||
@@ -7267,7 +7254,7 @@ body {
|
||||
--color-background-strong: #ffffff;
|
||||
--color-background-strong-hover: #eeeeee;
|
||||
--color-text: #d4d4d4;
|
||||
--color-text-weak: #a3a3a3;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-strong: #ffffff;
|
||||
--color-text-inverted: #161616;
|
||||
--color-border-weak: #ffffff1a;
|
||||
@@ -7278,12 +7265,11 @@ body {
|
||||
--stats-line-strong: #ffffff33;
|
||||
--stats-text: #ffffff;
|
||||
--stats-muted: #d4d4d4;
|
||||
--stats-faint: #a3a3a3;
|
||||
--stats-accent-text: #8190ff;
|
||||
--stats-faint: #808080;
|
||||
--stats-theme-icon-active: #fafafa;
|
||||
--stats-bar-idle: #303030;
|
||||
--stats-dot: #303030;
|
||||
--stats-hero-muted: #a3a3a3;
|
||||
--stats-hero-muted: #808080;
|
||||
--stats-hero-pattern: #303030;
|
||||
--stats-logo-bg: #f1ecec;
|
||||
--stats-logo-fill: #b7b1b1;
|
||||
@@ -8407,7 +8393,7 @@ body {
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] {
|
||||
--top-models-mobile-bar-width: 18px;
|
||||
--top-models-mobile-bar-width: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] [data-slot="top-models-axis"],
|
||||
@@ -8416,7 +8402,7 @@ body {
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="market-share"][data-dense-labels="true"] {
|
||||
--market-mobile-bar-width: 18px;
|
||||
--market-mobile-bar-width: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="market-share"][data-dense-labels="true"] [data-slot="market-labels"],
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { Link, Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { scaleSqrt } from "d3-scale"
|
||||
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
|
||||
import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url"
|
||||
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
|
||||
import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url"
|
||||
import ibmPlexMonoBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Bold-Latin1.woff2?url"
|
||||
import {
|
||||
getStatsHomeData,
|
||||
type CacheRatioEntry,
|
||||
type CountryEntry,
|
||||
type LeaderboardEntry,
|
||||
type MarketDay,
|
||||
type StatsHomeData,
|
||||
type SessionCostEntry,
|
||||
type TokenCostEntry,
|
||||
type UsagePoint,
|
||||
@@ -15,13 +23,14 @@ import {
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
import { runStatsEffect } from "../stats-runtime"
|
||||
import { LocaleLinks } from "../component/locale-links"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { useLanguage } from "../context/language"
|
||||
import { localizedUrl } from "../lib/language"
|
||||
import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
|
||||
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||
import { SectionHeading } from "./section-heading"
|
||||
import { setStatsPageCacheHeaders } from "./stats-cache"
|
||||
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
|
||||
@@ -36,6 +45,9 @@ import {
|
||||
type ThemePreference,
|
||||
} from "./stats-shell"
|
||||
|
||||
const products = ["All Users", "Zen", "Go"] as const
|
||||
const tokenProducts = ["Zen", "Go"] as const
|
||||
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
|
||||
const comparisonPairIndexes = [
|
||||
[0, 1, "Top two by recent usage"],
|
||||
[0, 2, "Leader vs challenger"],
|
||||
@@ -57,40 +69,60 @@ const usageColors = [
|
||||
"#ff6467",
|
||||
]
|
||||
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
||||
const geoMapWidth = 960
|
||||
const geoMapHeight = 430
|
||||
|
||||
type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M"
|
||||
type UsageProduct = (typeof products)[number]
|
||||
type TokenProduct = (typeof tokenProducts)[number]
|
||||
type UsageRange = (typeof ranges)[number]
|
||||
type IsoCountryCode = readonly [string, string, string]
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
|
||||
type StatsHomePageData = {
|
||||
updatedAt: string | null
|
||||
usage: UsagePoint[]
|
||||
users: UsagePoint[]
|
||||
leaderboard: LeaderboardEntry[]
|
||||
market: MarketDay[]
|
||||
tokenCost: TokenCostEntry[]
|
||||
cacheRatio: CacheRatioEntry[]
|
||||
sessionCost: SessionCostEntry[]
|
||||
country: CountryEntry[]
|
||||
function productLabel(product: UsageProduct | TokenProduct, i18n: ReturnType<typeof useI18n>) {
|
||||
if (product === "All Users") return i18n.t("product.allUsers")
|
||||
if (product === "Zen") return i18n.t("product.zen")
|
||||
return i18n.t("product.go")
|
||||
}
|
||||
|
||||
function rangeLabel(range: UsageRange, i18n: ReturnType<typeof useI18n>) {
|
||||
if (range === "1D") return i18n.t("range.1D")
|
||||
if (range === "1W") return i18n.t("range.1W")
|
||||
if (range === "2W") return i18n.t("range.2W")
|
||||
if (range === "1M") return i18n.t("range.1M")
|
||||
return i18n.t("range.2M")
|
||||
}
|
||||
|
||||
const countryNumericIds = new Map(
|
||||
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
|
||||
)
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
marker: geoCountryMarker(country),
|
||||
}))
|
||||
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
const getData = query(async () => {
|
||||
"use server"
|
||||
const [stats, catalog] = await Promise.all([runStatsEffect(getStatsHomeData()), loadModelCatalog()])
|
||||
return {
|
||||
updatedAt: stats.updatedAt,
|
||||
usage: stats.usage.Go["2M"],
|
||||
users: stats.users.Go["2M"],
|
||||
leaderboard: stats.leaderboard.Go["2M"],
|
||||
market: stats.market["2M"],
|
||||
tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
|
||||
cacheRatio: stats.cacheRatio.Go,
|
||||
sessionCost: stats.sessionCost.Go,
|
||||
country: stats.country["2M"],
|
||||
} satisfies StatsHomePageData
|
||||
return runStatsEffect(getStatsHomeData())
|
||||
}, "getStatsHomeData")
|
||||
|
||||
export default function StatsHome() {
|
||||
@@ -101,6 +133,7 @@ export default function StatsHome() {
|
||||
const statsHomeUrl = localizedUrl(language.locale(), "/data/")
|
||||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||
const data = createAsync(() => getData())
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
@@ -138,6 +171,10 @@ export default function StatsHome() {
|
||||
<Meta name="twitter:description" content={i18n.t("app.description")} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Link rel="preload" href={ibmPlexMonoRegularLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoMediumLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoSemiBoldLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoBoldLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Header githubStars={githubStars() ?? githubLink.fallbackStars} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
@@ -148,12 +185,12 @@ export default function StatsHome() {
|
||||
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
|
||||
<UniqueUsersSection data={stats().users} />
|
||||
<SessionCostSection data={stats().sessionCost} />
|
||||
<TokenCostSection data={stats().tokenCost} />
|
||||
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
|
||||
<CacheRatioSection data={stats().cacheRatio} />
|
||||
<MarketShareSection data={stats().market} />
|
||||
<GeoBreakdownSection data={stats().country} />
|
||||
<ComparisonCardsSection
|
||||
pairs={homeComparisonPairs(stats().leaderboard)}
|
||||
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
|
||||
title="Model Comparisons"
|
||||
description="Popular model pairs from the leaderboard."
|
||||
variant="featured"
|
||||
@@ -219,8 +256,7 @@ function Hero(props: { updatedAt: string | null }) {
|
||||
|
||||
return (
|
||||
<section data-section="hero">
|
||||
<p data-slot="hero-meta" role="status" aria-atomic="true">
|
||||
<span data-slot="visually-hidden">{currentUpdatedLabel()}</span>
|
||||
<p data-slot="hero-meta" aria-live="polite" aria-atomic="true" aria-label={currentUpdatedLabel()}>
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -365,9 +401,32 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
|
||||
return `${value.date}, ${value.time}`
|
||||
}
|
||||
|
||||
function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardEntry[] }) {
|
||||
function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<UsageProduct>("Go")
|
||||
const [range, setRange] = createSignal<UsageRange>("2M")
|
||||
const [sheet, setSheet] = createSignal<"product" | "range">()
|
||||
const [activeModel, setActiveModel] = createSignal<string>()
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
const leaderboard = createMemo(() => props.leaderboard[product()][range()])
|
||||
|
||||
createEffect(() => {
|
||||
if (!sheet()) return
|
||||
if (typeof document === "undefined") return
|
||||
const htmlOverflow = document.documentElement.style.overflow
|
||||
const bodyOverflow = document.body.style.overflow
|
||||
document.documentElement.style.overflow = "hidden"
|
||||
document.body.style.overflow = "hidden"
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setSheet(undefined)
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown)
|
||||
onCleanup(() => {
|
||||
document.documentElement.style.overflow = htmlOverflow
|
||||
document.body.style.overflow = bodyOverflow
|
||||
document.removeEventListener("keydown", onKeyDown)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<section id="top-models" data-section="top-models">
|
||||
@@ -379,23 +438,201 @@ function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardE
|
||||
description={i18n.t("home.topModelsDescription")}
|
||||
/>
|
||||
<Show
|
||||
when={props.data.some((item) => usageTotal(item) > 0)}
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
|
||||
>
|
||||
<TopModelsChart data={props.data} range="2M" activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
<TopModelsChart
|
||||
data={data()}
|
||||
range={range()}
|
||||
activeModel={activeModel()}
|
||||
onActiveModelChange={setActiveModel}
|
||||
/>
|
||||
</Show>
|
||||
<Show
|
||||
when={props.leaderboard.length > 0}
|
||||
when={leaderboard().length > 0}
|
||||
fallback={
|
||||
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
|
||||
}
|
||||
>
|
||||
<Leaderboard data={props.leaderboard} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
<Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer" hidden>
|
||||
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
<div data-slot="top-models-mobile-controls">
|
||||
<MobileFilterButton
|
||||
label={i18n.t("home.productFilter")}
|
||||
value={productLabel(product(), i18n)}
|
||||
expanded={sheet() === "product"}
|
||||
onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
|
||||
/>
|
||||
<MobileFilterButton
|
||||
label={i18n.t("home.dateRange")}
|
||||
value={rangeLabel(range(), i18n)}
|
||||
expanded={sheet() === "range"}
|
||||
onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sheet()}>
|
||||
{(kind) => (
|
||||
<MobileFilterSheet
|
||||
kind={kind()}
|
||||
product={product()}
|
||||
range={range()}
|
||||
onProductSelect={(value) => {
|
||||
setProduct(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onRangeSelect={(value) => {
|
||||
setRange(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onClose={() => setSheet(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
data-slot="mobile-filter-button"
|
||||
type="button"
|
||||
aria-label={props.label}
|
||||
aria-expanded={props.expanded ? "true" : "false"}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<span>{props.value}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterSheet(props: {
|
||||
kind: "product" | "range"
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
|
||||
<div
|
||||
data-slot="filter-sheet-panel"
|
||||
role="radiogroup"
|
||||
aria-label={props.kind === "product" ? i18n.t("home.productFilter") : i18n.t("home.dateRange")}
|
||||
>
|
||||
<Show
|
||||
when={props.kind === "product"}
|
||||
fallback={
|
||||
<For each={ranges}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.range === item}
|
||||
data-active={props.range === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onRangeSelect(item)
|
||||
}}
|
||||
>
|
||||
{rangeLabel(item, i18n)}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<For each={products}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.product === item}
|
||||
data-active={props.product === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onProductSelect(item)
|
||||
}}
|
||||
>
|
||||
{productLabel(item, i18n)}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDown() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
||||
<path d="M5 7L8 10L11 7" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsFilters(props: {
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<>
|
||||
<FilterPills
|
||||
items={products}
|
||||
selected={props.product}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={props.onProductSelect}
|
||||
/>
|
||||
<FilterPills
|
||||
items={ranges}
|
||||
selected={props.range}
|
||||
label={i18n.t("home.dateRange")}
|
||||
variant="range"
|
||||
formatLabel={(item) => rangeLabel(item, i18n)}
|
||||
onSelect={props.onRangeSelect}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterPills<T extends string>(props: {
|
||||
items: readonly T[]
|
||||
selected: T
|
||||
label: string
|
||||
variant: "product" | "range"
|
||||
formatLabel?: (item: T) => string
|
||||
onSelect: (item: T) => void
|
||||
}) {
|
||||
return (
|
||||
<div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.selected === item}
|
||||
data-active={props.selected === item ? "true" : undefined}
|
||||
onClick={() => props.onSelect(item)}
|
||||
>
|
||||
{props.formatLabel ? props.formatLabel(item) : item}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TopModelsChart(props: {
|
||||
data: UsagePoint[]
|
||||
range: UsageRange
|
||||
@@ -586,9 +823,10 @@ function TopModelsChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function UniqueUsersSection(props: { data: UsagePoint[] }) {
|
||||
function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
|
||||
const i18n = useI18n()
|
||||
const [activeModel, setActiveModel] = createSignal<string>()
|
||||
const data = createMemo(() => props.data.Go["2M"])
|
||||
|
||||
return (
|
||||
<section id="unique-users" data-section="unique-users">
|
||||
@@ -599,13 +837,13 @@ function UniqueUsersSection(props: { data: UsagePoint[] }) {
|
||||
description={i18n.t("home.uniqueUsersDescription")}
|
||||
/>
|
||||
<Show
|
||||
when={props.data.some((item) => usageTotal(item) > 0)}
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
fallback={
|
||||
<EmptyState title={i18n.t("home.noUserDataTitle")} description={i18n.t("home.noUserDataDescription")} />
|
||||
}
|
||||
>
|
||||
<TopModelsChart
|
||||
data={props.data}
|
||||
data={data()}
|
||||
range="2M"
|
||||
metric="users"
|
||||
ariaLabel={i18n.t("home.uniqueUsersChart")}
|
||||
@@ -689,7 +927,7 @@ function isDenseColumnRange(range: UsageRange) {
|
||||
function scrollDenseChartToEnd(element: HTMLDivElement | undefined, range: UsageRange, count: number) {
|
||||
if (!element || count <= 0 || !isDenseColumnRange(range) || typeof window === "undefined") return
|
||||
window.requestAnimationFrame(() => {
|
||||
element.scrollLeft = Number.MAX_SAFE_INTEGER
|
||||
element.scrollLeft = element.scrollWidth - element.clientWidth
|
||||
})
|
||||
}
|
||||
|
||||
@@ -728,11 +966,12 @@ function Leaderboard(props: {
|
||||
activeModel: string | undefined
|
||||
onActiveModelChange: (model: string | undefined) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const featured = createMemo(() => props.data.slice(0, 3))
|
||||
const compact = createMemo(() => props.data.slice(3))
|
||||
|
||||
return (
|
||||
<div id="leaderboard" data-component="leaderboard">
|
||||
<div id="leaderboard" data-component="leaderboard" role="list" aria-label={i18n.t("chart.leaderboardAria")}>
|
||||
<div data-slot="leaderboard-featured">
|
||||
<For each={featured()}>
|
||||
{(entry) => (
|
||||
@@ -758,7 +997,7 @@ function Leaderboard(props: {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div data-slot="leaderboard-mobile">
|
||||
<div data-slot="leaderboard-mobile" aria-label={i18n.t("chart.scrollableLeaderboardAria")}>
|
||||
<For each={props.data}>
|
||||
{(entry) => (
|
||||
<LeaderboardCard
|
||||
@@ -790,6 +1029,9 @@ function LeaderboardCard(props: {
|
||||
href={language.route(
|
||||
`${import.meta.env.BASE_URL}${modelSlug(props.entry.provider)}/${modelSlug(props.entry.model)}`,
|
||||
)}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-label={`${String(props.entry.rank).padStart(2, "0")} ${props.entry.model} ${i18n.t("chart.byAuthor", { author: props.entry.author })}`}
|
||||
onPointerEnter={() => props.onActiveModelChange(props.entry.model)}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
@@ -842,14 +1084,16 @@ function formatChange(value: number | null, i18n: ReturnType<typeof useI18n>) {
|
||||
return `${value}%`
|
||||
}
|
||||
|
||||
function MarketShareSection(props: { data: MarketDay[] }) {
|
||||
function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
||||
const i18n = useI18n()
|
||||
const [range, setRange] = createSignal<UsageRange>("2M")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const [activeAuthor, setActiveAuthor] = createSignal<string>()
|
||||
const [inspecting, setInspecting] = createSignal(false)
|
||||
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
|
||||
const activeDay = createMemo(() => props.data[selectedIndex()])
|
||||
const data = createMemo(() => props.data[range()])
|
||||
const authorOrder = createMemo(() => getMarketAuthorOrder(data()))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
const activeDay = createMemo(() => data()[selectedIndex()])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -874,8 +1118,8 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
||||
{(day) => (
|
||||
<>
|
||||
<MarketShare
|
||||
data={props.data}
|
||||
range="2M"
|
||||
data={data()}
|
||||
range={range()}
|
||||
authorOrder={authorOrder()}
|
||||
activeIndex={selectedIndex()}
|
||||
activeAuthor={activeAuthor()}
|
||||
@@ -907,9 +1151,22 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
||||
<strong>
|
||||
{inspecting()
|
||||
? formatMarketDate(activeDay(), i18n.t("home.noData"))
|
||||
: formatMarketRange(props.data, i18n.t("home.noData"))}
|
||||
: formatMarketRange(data(), i18n.t("home.noData"))}
|
||||
</strong>
|
||||
</p>
|
||||
<div hidden>
|
||||
<FilterPills
|
||||
items={ranges}
|
||||
selected={range()}
|
||||
label={i18n.t("home.dateRange")}
|
||||
variant="range"
|
||||
onSelect={(item) => {
|
||||
setRange(item)
|
||||
setActiveAuthor(undefined)
|
||||
setInspecting(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -945,7 +1202,6 @@ function MarketShare(props: {
|
||||
{(day, index) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${day.date} ${formatTrillions(day.total)}`}
|
||||
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
||||
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||
@@ -1021,11 +1277,14 @@ function MarketShareList(props: {
|
||||
activeAuthor: string | undefined
|
||||
onActiveAuthorChange: (author: string) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<ol data-component="market-share-list">
|
||||
<For each={props.data}>
|
||||
{(item, index) => {
|
||||
const label = () =>
|
||||
`${item.author} ${formatTrillions(item.tokens)} ${item.share.toFixed(1)} ${i18n.t("chart.percent")}`
|
||||
const content = () => (
|
||||
<>
|
||||
<span>{String(index() + 1).padStart(2, "0")}</span>
|
||||
@@ -1047,6 +1306,7 @@ function MarketShareList(props: {
|
||||
fallback={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label()}
|
||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
||||
>
|
||||
@@ -1057,6 +1317,7 @@ function MarketShareList(props: {
|
||||
{(href) => (
|
||||
<a
|
||||
href={href()}
|
||||
aria-label={label()}
|
||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
||||
>
|
||||
@@ -1072,22 +1333,23 @@ function MarketShareList(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function GeoBreakdownSection(props: { data: CountryEntry[] }) {
|
||||
function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const [activeCountry, setActiveCountry] = createSignal<string>()
|
||||
const data = createMemo(() => props.data["2M"])
|
||||
const countryById = createMemo(
|
||||
() =>
|
||||
new Map(
|
||||
props.data.flatMap((country) => {
|
||||
data().flatMap((country) => {
|
||||
const id = countryNumericId(country.country)
|
||||
return id ? [[id, country] as const] : []
|
||||
}),
|
||||
),
|
||||
)
|
||||
const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
|
||||
const topCountries = createMemo(() => props.data.slice(0, 15))
|
||||
const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
|
||||
const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
|
||||
const topCountries = createMemo(() => data().slice(0, 15))
|
||||
const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -1101,7 +1363,7 @@ function GeoBreakdownSection(props: { data: CountryEntry[] }) {
|
||||
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
|
||||
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
|
||||
<Show
|
||||
when={props.data.length > 0}
|
||||
when={data().length > 0}
|
||||
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
|
||||
>
|
||||
<div data-component="geo-breakdown">
|
||||
@@ -1191,30 +1453,31 @@ function GeoWorldMap(props: {
|
||||
</For>
|
||||
</g>
|
||||
<g data-slot="geo-country-markers">
|
||||
<For each={worldCountryMarkers}>
|
||||
<For each={worldCountryPaths}>
|
||||
{(country) => {
|
||||
const entry = () => props.countryById.get(country.id)
|
||||
return (
|
||||
<Show when={entry()}>
|
||||
<circle
|
||||
cx={country.marker.x}
|
||||
cy={country.marker.y}
|
||||
data-country-id={country.id}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
<Show when={country.marker && entry() ? country.marker : undefined}>
|
||||
{(marker) => (
|
||||
<circle
|
||||
cx={marker().x}
|
||||
cy={marker().y}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
@@ -1244,6 +1507,7 @@ function GeoCountryList(props: {
|
||||
type="button"
|
||||
data-active={props.activeCountry === country.country ? "true" : undefined}
|
||||
style={{ "--geo-row-opacity": String(opacityScale()(country.tokens)) } as JSX.CSSProperties}
|
||||
aria-label={`${formatCountryName(country.country, language.tag(language.locale()), i18n.t("home.unknown"))} ${formatGeoTokens(country.tokens)} ${formatGeoShare(country.share)}`}
|
||||
onClick={() => props.onActiveCountryChange(country.country)}
|
||||
onPointerEnter={() => props.onActiveCountryChange(country.country)}
|
||||
onFocus={() => props.onActiveCountryChange(country.country)}
|
||||
@@ -1267,6 +1531,14 @@ function countryNumericId(country: string) {
|
||||
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
|
||||
}
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function formatCountryName(country: string, locale: string, unknown: string) {
|
||||
const code = country.toUpperCase()
|
||||
if (code === "ZZ") return unknown
|
||||
@@ -1364,10 +1636,12 @@ function marketDateParts(label: string) {
|
||||
return { start: start ?? label, end: end ?? start ?? label }
|
||||
}
|
||||
|
||||
function TokenCostSection(props: { data: TokenCostEntry[] }) {
|
||||
function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const visible = createMemo(() => props.data.slice(0, 13))
|
||||
const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
|
||||
const visible = createMemo(() => data().slice(0, 13))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1386,6 +1660,17 @@ function TokenCostSection(props: { data: TokenCostEntry[] }) {
|
||||
>
|
||||
<TokenCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1438,10 +1723,12 @@ function TokenCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
|
||||
function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const visible = createMemo(() => props.data.slice(0, 16))
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const visible = createMemo(() => data().slice(0, 16))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1458,6 +1745,17 @@ function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
|
||||
>
|
||||
<CacheRatioChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1555,10 +1853,12 @@ function MetricBar(props: { value: number; max: number; active: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostSection(props: { data: SessionCostEntry[] }) {
|
||||
function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const visible = createMemo(() => props.data.slice(0, 16))
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const visible = createMemo(() => data().slice(0, 16))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1577,6 +1877,17 @@ function SessionCostSection(props: { data: SessionCostEntry[] }) {
|
||||
>
|
||||
<SessionCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1638,6 +1949,11 @@ function SessionCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function LiveIndicator() {
|
||||
const i18n = useI18n()
|
||||
return <span data-component="live-filter">{i18n.t("chart.live")}</span>
|
||||
}
|
||||
|
||||
function formatTokenCount(value: number) {
|
||||
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
|
||||
return `${Math.round(value / 1_000)}K`
|
||||
|
||||
@@ -10,7 +10,7 @@ export type HeaderLink = { href: string; label: string }
|
||||
export const githubLink = {
|
||||
href: "https://github.com/anomalyco/opencode",
|
||||
apiHref: "https://api.github.com/repos/anomalyco/opencode",
|
||||
fallbackStars: "195K",
|
||||
fallbackStars: "150K",
|
||||
}
|
||||
export const themePreferences = ["dark", "light", "system"] as const
|
||||
export const themeStorageKey = "opencode:stats-theme"
|
||||
@@ -18,8 +18,7 @@ export type ThemePreference = (typeof themePreferences)[number]
|
||||
|
||||
const compactNumberFormatter = new Intl.NumberFormat("en", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 0,
|
||||
roundingIncrement: 5,
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
export const getGitHubStars = query(async () => {
|
||||
|
||||
@@ -18,7 +18,6 @@ export default defineConfig({
|
||||
allowedHosts: true,
|
||||
},
|
||||
build: {
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
minify: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user