mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 01:29:44 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65cf14df16 |
@@ -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 }}
|
||||
|
||||
+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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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"
|
||||
@@ -123,8 +120,7 @@ export function SessionContextTab() {
|
||||
() => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
@@ -224,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
|
||||
@@ -357,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
|
||||
|
||||
@@ -15,12 +15,12 @@ const rootSession = (input: { id: string; parentID?: string; archived?: number }
|
||||
},
|
||||
}) as Session
|
||||
|
||||
const userMessage = (id: string, sessionID: string, created = 1) =>
|
||||
const userMessage = (id: string, sessionID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created },
|
||||
time: { created: 1 },
|
||||
agent: "assistant",
|
||||
model: { providerID: "openai", modelID: "gpt" },
|
||||
}) as Message
|
||||
@@ -370,13 +370,13 @@ describe("applyDirectoryEvent", () => {
|
||||
const sessionID = "ses_1"
|
||||
const [store, setStore] = createStore(
|
||||
baseState({
|
||||
message: { [sessionID]: [userMessage("msg_z", sessionID, 1), userMessage("msg_b", sessionID, 3)] },
|
||||
part: { msg_a: [textPart("prt_1", sessionID, "msg_a")] },
|
||||
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
|
||||
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
|
||||
}),
|
||||
)
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "message.updated", properties: { info: userMessage("msg_a", sessionID, 2) } },
|
||||
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
@@ -384,14 +384,14 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a", "msg_b"])
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
...userMessage("msg_a", sessionID, 2),
|
||||
...userMessage("msg_2", sessionID),
|
||||
role: "assistant",
|
||||
} as Message,
|
||||
},
|
||||
@@ -403,10 +403,10 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.find((x) => x.id === "msg_a")?.role).toBe("assistant")
|
||||
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_a" } },
|
||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
@@ -414,8 +414,8 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_b"])
|
||||
expect(store.part.msg_a).toBeUndefined()
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
|
||||
expect(store.part.msg_2).toBeUndefined()
|
||||
})
|
||||
|
||||
test("upserts and prunes message parts", () => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { State, VcsCache } from "./types"
|
||||
import { trimSessions } from "./session-trim"
|
||||
import { dropSessionCaches } from "./session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { messageKey } from "@/utils/session-message"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const SESSION_CONTENT_EVENTS = new Set([
|
||||
@@ -276,7 +275,7 @@ export function applyDirectoryEvent(input: {
|
||||
input.setStore("message", info.sessionID, [info])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||
const result = Binary.search(messages, info.id, (m) => m.id)
|
||||
if (result.found) {
|
||||
input.setStore("message", info.sessionID, result.index, reconcile(info))
|
||||
break
|
||||
@@ -296,8 +295,8 @@ export function applyDirectoryEvent(input: {
|
||||
produce((draft) => {
|
||||
const messages = draft.message[props.sessionID]
|
||||
if (messages) {
|
||||
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
const parts = draft.part[props.messageID]
|
||||
if (parts) {
|
||||
@@ -323,7 +322,7 @@ export function applyDirectoryEvent(input: {
|
||||
input.setStore("part", part.messageID, [part])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(parts, part.id, (item) => item.id)
|
||||
const result = Binary.search(parts, part.id, (p) => p.id)
|
||||
if (result.found) {
|
||||
input.setStore("part", part.messageID, result.index, reconcile(part))
|
||||
break
|
||||
@@ -346,13 +345,13 @@ export function applyDirectoryEvent(input: {
|
||||
)
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
if (result.found) {
|
||||
input.setStore(
|
||||
produce((draft) => {
|
||||
const list = draft.part[props.messageID]
|
||||
if (!list) return
|
||||
const next = Binary.search(list, props.partID, (part) => part.id)
|
||||
const next = Binary.search(list, props.partID, (p) => p.id)
|
||||
if (!next.found) return
|
||||
list.splice(next.index, 1)
|
||||
if (list.length === 0) delete draft.part[props.messageID]
|
||||
@@ -365,7 +364,7 @@ export function applyDirectoryEvent(input: {
|
||||
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
if (!result.found) break
|
||||
const field = props.field as keyof (typeof parts)[number]
|
||||
const current = parts[result.index]?.[field]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -264,7 +264,6 @@ describe("server session", () => {
|
||||
|
||||
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
})
|
||||
|
||||
test("extends a current page to include the user for split assistant turns", async () => {
|
||||
@@ -1498,7 +1497,7 @@ describe("server session", () => {
|
||||
|
||||
await store.sync("child", { force: true })
|
||||
|
||||
expect(store.data.message.child).toEqual([older, boundary])
|
||||
expect(store.data.message.child).toEqual([boundary, older])
|
||||
})
|
||||
|
||||
test("preserves a part update for a message being loaded from history", async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { message as cleanMessage } from "@/utils/diffs"
|
||||
import { sessionNotFoundError } from "@/utils/server-errors"
|
||||
import { rootSession } from "@/utils/session-route"
|
||||
import { normalizeSessionInfo } from "@/utils/session"
|
||||
import { compareMessages, messageKey, normalizeSessionMessages } from "@/utils/session-message"
|
||||
import { normalizeSessionMessages } from "@/utils/session-message"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
||||
import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
|
||||
import type { ServerApi } from "@/utils/server"
|
||||
@@ -26,6 +26,7 @@ import type { ServerApi } from "@/utils/server"
|
||||
type MessageApi = ServerApi["message"]
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const initialMessagePageSize = 20
|
||||
const historyMessagePageSize = 200
|
||||
@@ -63,7 +64,7 @@ type MessagePage = {
|
||||
function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
|
||||
return items
|
||||
.slice()
|
||||
.sort((a, b) => compareMessages(a.info, b.info))
|
||||
.sort((a, b) => cmp(a.info.id, b.info.id))
|
||||
.map((item) => {
|
||||
if (item.info.role === "user") {
|
||||
return {
|
||||
@@ -110,16 +111,17 @@ function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
||||
const observed: { messageID: string; parts: Part[] }[] = []
|
||||
for (const item of items) {
|
||||
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||
const found = result.found
|
||||
if (!found) session.splice(result.index, 0, item.message)
|
||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||
if (!result.found) session.splice(result.index, 0, item.message)
|
||||
const current = part.get(item.message.id)
|
||||
const confirmed = found ? item.parts.filter((part) => current?.some((value) => value.id === part.id)) : []
|
||||
if (found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||
const confirmed = result.found
|
||||
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
|
||||
: []
|
||||
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||
part.set(
|
||||
item.message.id,
|
||||
merge(
|
||||
found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||
item.parts.filter((part) => !confirmed.includes(part)),
|
||||
),
|
||||
)
|
||||
@@ -156,7 +158,6 @@ function reconcileFetched<T extends { id: string }>(
|
||||
retained?: ReadonlySet<string>
|
||||
removed?: ReadonlySet<string>
|
||||
preserveUnfetched?: boolean | ((item: T) => boolean)
|
||||
compare?: (a: T, b: T) => number
|
||||
} = {},
|
||||
) {
|
||||
const result = new Map(fetched.map((item) => [item.id, item]))
|
||||
@@ -179,8 +180,7 @@ function reconcileFetched<T extends { id: string }>(
|
||||
if (!item) result.delete(id)
|
||||
}
|
||||
for (const id of options.removed ?? emptyIDs) result.delete(id)
|
||||
const items = [...result.values()]
|
||||
return options.compare ? items.sort(options.compare) : items
|
||||
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
|
||||
}
|
||||
|
||||
type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
|
||||
@@ -413,7 +413,8 @@ export function createServerSession(
|
||||
if (!load) return
|
||||
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
|
||||
const messages = data.message[sessionID]
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
const parts = load.touchedParts.get(messageID)
|
||||
if (parts) {
|
||||
parts.add(partID)
|
||||
@@ -436,14 +437,16 @@ export function createServerSession(
|
||||
load.touchedParts.set(messageID, new Set(parts))
|
||||
load.carriedDeltaParts.set(messageID, new Set(parts))
|
||||
const messages = data.message[sessionID]
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
}
|
||||
for (const [messageID, parts] of load.removedParts) {
|
||||
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
||||
parts.forEach((partID) => touched.add(partID))
|
||||
load.touchedParts.set(messageID, touched)
|
||||
const messages = data.message[sessionID]
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
}
|
||||
for (const [messageID, parts] of load.optimisticParts) {
|
||||
load.removedMessages.delete(messageID)
|
||||
@@ -552,7 +555,7 @@ export function createServerSession(
|
||||
const source = pages.flatMap((page) => page.data).toReversed()
|
||||
const normalized = normalizeSessionMessages(sessionID, source)
|
||||
return {
|
||||
session: normalized.messages.sort(compareMessages),
|
||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||
part: [...normalized.parts.entries()]
|
||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||
.sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -569,7 +572,7 @@ export function createServerSession(
|
||||
})
|
||||
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
||||
return {
|
||||
session: items.map((item) => cleanMessage(item.info)).sort(compareMessages),
|
||||
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
||||
part: items.map((item) => ({
|
||||
id: item.info.id,
|
||||
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -693,7 +696,7 @@ export function createServerSession(
|
||||
const normalized = normalizeSessionMessages(sessionID, source)
|
||||
return {
|
||||
...page,
|
||||
session: normalized.messages.sort(compareMessages),
|
||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||
part: [...normalized.parts.entries()]
|
||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||
.sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -710,7 +713,6 @@ export function createServerSession(
|
||||
retained: load?.retainedMessages,
|
||||
removed: load?.removedMessages,
|
||||
preserveUnfetched,
|
||||
compare: compareMessages,
|
||||
})
|
||||
batch(() => {
|
||||
if (source) setData("session_message", sessionID, reconcile(source))
|
||||
@@ -752,7 +754,7 @@ export function createServerSession(
|
||||
try {
|
||||
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||
const first = page.session.reduce<Message | undefined>(
|
||||
(oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
|
||||
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
||||
undefined,
|
||||
)
|
||||
if (generations.get(sessionID) !== active) return
|
||||
@@ -802,15 +804,14 @@ export function createServerSession(
|
||||
session: merge(
|
||||
page.session,
|
||||
parents.map((parent) => parent.message),
|
||||
).sort(compareMessages),
|
||||
),
|
||||
part: merge(
|
||||
page.part,
|
||||
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
|
||||
),
|
||||
}
|
||||
const preserveUnfetched =
|
||||
mode === "prepend" ||
|
||||
(!result.complete && (!first || ((message: Message) => compareMessages(message, first) < 0)))
|
||||
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
||||
applyMessagePage(
|
||||
sessionID,
|
||||
result,
|
||||
@@ -927,7 +928,7 @@ export function createServerSession(
|
||||
.message({ sessionID, messageID })
|
||||
.then((message) => {
|
||||
const current = data.session_message[sessionID] ?? []
|
||||
const messages = [...current.filter((item) => item.id !== message.id), message].sort(compareMessages)
|
||||
const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id))
|
||||
projectV2({ sessionID, messages, touched: [message.id] })
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -1050,7 +1051,7 @@ export function createServerSession(
|
||||
setData("message", info.sessionID, [info])
|
||||
return
|
||||
}
|
||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||
const result = Binary.search(messages, info.id, (message) => message.id)
|
||||
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
||||
if (!result.found)
|
||||
setData("message", info.sessionID, (value = []) => {
|
||||
@@ -1083,8 +1084,8 @@ export function createServerSession(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[props.sessionID]
|
||||
if (messages) {
|
||||
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
deleteMessageParts(draft, props.messageID)
|
||||
}),
|
||||
@@ -1096,7 +1097,7 @@ export function createServerSession(
|
||||
if (SKIP_PARTS.has(part.type)) return
|
||||
const messages = data.message[part.sessionID]
|
||||
const load = messageLoads.get(part.sessionID)
|
||||
const missing = !messages?.some((message) => message.id === part.messageID)
|
||||
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
|
||||
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
||||
if (
|
||||
missing &&
|
||||
@@ -1340,7 +1341,7 @@ export function createServerSession(
|
||||
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
|
||||
if (!items)
|
||||
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
|
||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]).sort(compareMessages))
|
||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
||||
setData(
|
||||
"part_text_accum_delta",
|
||||
produce((draft) => {
|
||||
|
||||
@@ -4,11 +4,11 @@ import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "
|
||||
|
||||
type Text = Extract<Part, { type: "text" }>
|
||||
|
||||
const userMessage = (id: string, sessionID: string, created = 1): Message => ({
|
||||
const userMessage = (id: string, sessionID: string): Message => ({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created },
|
||||
time: { created: 1 },
|
||||
agent: "assistant",
|
||||
model: { providerID: "openai", modelID: "gpt" },
|
||||
})
|
||||
@@ -22,21 +22,21 @@ const textPart = (id: string, sessionID: string, messageID: string): Text => ({
|
||||
})
|
||||
|
||||
describe("sync optimistic reducers", () => {
|
||||
test("applyOptimisticAdd inserts by creation time", () => {
|
||||
test("applyOptimisticAdd inserts message in sorted order and stores parts", () => {
|
||||
const sessionID = "ses_1"
|
||||
const draft = {
|
||||
message: { [sessionID]: [userMessage("msg_z", sessionID, 1)] },
|
||||
message: { [sessionID]: [userMessage("msg_2", sessionID)] },
|
||||
part: {} as Record<string, Part[] | undefined>,
|
||||
}
|
||||
|
||||
applyOptimisticAdd(draft, {
|
||||
sessionID,
|
||||
message: userMessage("msg_a", sessionID, 2),
|
||||
parts: [textPart("prt_2", sessionID, "msg_a"), textPart("prt_1", sessionID, "msg_a")],
|
||||
message: userMessage("msg_1", sessionID),
|
||||
parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")],
|
||||
})
|
||||
|
||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||
expect(draft.part.msg_a?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||
expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||
})
|
||||
|
||||
test("applyOptimisticRemove removes message and part entries", () => {
|
||||
@@ -60,33 +60,19 @@ describe("sync optimistic reducers", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
{
|
||||
session: [userMessage("msg_z", sessionID, 1)],
|
||||
part: [{ id: "msg_z", part: [textPart("prt_1", sessionID, "msg_z")] }],
|
||||
session: [userMessage("msg_1", sessionID)],
|
||||
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
||||
complete: true,
|
||||
},
|
||||
[{ message: userMessage("msg_a", sessionID, 2), parts: [textPart("prt_2", sessionID, "msg_a")] }],
|
||||
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||
)
|
||||
|
||||
expect(page.session.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||
expect(page.part.find((x) => x.id === "msg_a")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||
expect(page.confirmed).toEqual([])
|
||||
expect(page.complete).toBe(true)
|
||||
})
|
||||
|
||||
test("mergeOptimisticPage uses IDs only to break equal-time ties", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
{
|
||||
session: [userMessage("msg_z", sessionID, 1)],
|
||||
part: [],
|
||||
complete: true,
|
||||
},
|
||||
[{ message: userMessage("msg_a", sessionID, 1), parts: [] }],
|
||||
)
|
||||
|
||||
expect(page.session.map((message) => message.id)).toEqual(["msg_a", "msg_z"])
|
||||
})
|
||||
|
||||
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createMemo } from "solid-js"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { messageKey } from "@/utils/session-message"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
@@ -68,7 +67,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
||||
const confirmed: string[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||
const found = result.found
|
||||
if (!found) session.splice(result.index, 0, item.message)
|
||||
|
||||
@@ -93,7 +92,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageKey(input.message), messageKey)
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
messages.splice(result.index, 0, input.message)
|
||||
} else {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
@@ -104,8 +103,8 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI
|
||||
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const index = messages.findIndex((message) => message.id === input.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
delete draft.part[input.messageID]
|
||||
}
|
||||
|
||||
@@ -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": "刪除",
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { LocalProject } from "@/context/layout"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { sessionHasOpenTab, useTabs } from "@/context/tabs"
|
||||
import { compareSessionTime, displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
||||
import { displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { showToast } from "@/utils/toast"
|
||||
@@ -254,7 +254,7 @@ function buildHomeSessionRecords(input: {
|
||||
const directories = new Set(input.projectDirectories().map(pathKey))
|
||||
const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory)))
|
||||
return [...new Map(sessions.map((session) => [session.id, session] as const)).values()]
|
||||
.sort(compareSessionTime)
|
||||
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.flatMap((session) => {
|
||||
const directory = pathKey(session.directory)
|
||||
const project =
|
||||
|
||||
@@ -10,7 +10,6 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
childSessionOnPath,
|
||||
closeHomeProject,
|
||||
compareSessionTime,
|
||||
displayName,
|
||||
effectiveWorkspaceOrder,
|
||||
errorMessage,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
homeProjectDirectories,
|
||||
homeSessionServerStatus,
|
||||
latestRootSession,
|
||||
sortedRootSessions,
|
||||
toggleHomeProjectSelection,
|
||||
} from "./helpers"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
@@ -155,30 +153,6 @@ describe("layout workspace helpers", () => {
|
||||
expect(result?.id).toBe("workspace")
|
||||
})
|
||||
|
||||
test("sorts recent sessions by persisted update time instead of id", () => {
|
||||
const result = sortedRootSessions(
|
||||
{
|
||||
path: { directory: "/workspace" },
|
||||
session: [
|
||||
session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
|
||||
session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 3, archived: undefined } }),
|
||||
],
|
||||
},
|
||||
3,
|
||||
)
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(["ses_a", "ses_z"])
|
||||
})
|
||||
|
||||
test("uses id only to break equal session timestamps", () => {
|
||||
const sessions = [
|
||||
session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
|
||||
session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
|
||||
]
|
||||
|
||||
expect(sessions.sort(compareSessionTime).map((item) => item.id)).toEqual(["ses_a", "ses_z"])
|
||||
})
|
||||
|
||||
test("detects project permissions with a filter", () => {
|
||||
const result = hasProjectPermissions(
|
||||
{
|
||||
|
||||
@@ -9,10 +9,18 @@ type SessionStore = {
|
||||
path: { directory: string }
|
||||
}
|
||||
|
||||
export function compareSessionTime(a: Session, b: Session) {
|
||||
const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)
|
||||
if (updated !== 0) return updated
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||
function sortSessions(now: number) {
|
||||
const oneMinuteAgo = now - 60 * 1000
|
||||
return (a: Session, b: Session) => {
|
||||
const aUpdated = a.time.updated ?? a.time.created
|
||||
const bUpdated = b.time.updated ?? b.time.created
|
||||
const aRecent = aUpdated > oneMinuteAgo
|
||||
const bRecent = bUpdated > oneMinuteAgo
|
||||
if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||
if (aRecent && !bRecent) return -1
|
||||
if (!aRecent && bRecent) return 1
|
||||
return bUpdated - aUpdated
|
||||
}
|
||||
}
|
||||
|
||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
@@ -21,10 +29,10 @@ const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
export const roots = (store: SessionStore) =>
|
||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||
|
||||
export const sortedRootSessions = (store: SessionStore, _now: number) => roots(store).sort(compareSessionTime)
|
||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||
|
||||
export const latestRootSession = (stores: SessionStore[], _now: number) =>
|
||||
stores.flatMap(roots).sort(compareSessionTime)[0]
|
||||
export const latestRootSession = (stores: SessionStore[], now: number) =>
|
||||
stores.flatMap(roots).sort(sortSessions(now))[0]
|
||||
|
||||
export function hasProjectPermissions<T>(
|
||||
request: Record<string, T[] | undefined> | undefined,
|
||||
|
||||
@@ -1851,9 +1851,7 @@ export default function Page() {
|
||||
|
||||
const session = sdk().api.session
|
||||
const target = sync()
|
||||
const index = userMessages().findIndex((item) => item.id === id)
|
||||
if (index < 0) return
|
||||
const next = userMessages()[index + 1]
|
||||
const next = userMessages().find((item) => item.id > id)
|
||||
const last = target.session.get(sessionID)?.revert
|
||||
|
||||
await runPromptRollbackMutation({
|
||||
@@ -1893,10 +1891,8 @@ export default function Page() {
|
||||
const rolled = createMemo(() => {
|
||||
const id = revertMessageID()
|
||||
if (!id) return []
|
||||
const index = userMessages().findIndex((item) => item.id === id)
|
||||
if (index < 0) return []
|
||||
return userMessages()
|
||||
.slice(index)
|
||||
.filter((item) => item.id >= id)
|
||||
.map((item) => ({ id: item.id, text: line(item.id) }))
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -287,9 +286,7 @@ export function MessageTimeline(props: {
|
||||
const visible = new Set(props.userMessages.map((message) => message.id))
|
||||
const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
|
||||
const messages = sync().data.session_message[id] ?? []
|
||||
if (!boundary) return messages
|
||||
const index = messages.findIndex((message) => message.id === boundary)
|
||||
return index < 0 ? messages : messages.slice(0, index)
|
||||
return boundary ? messages.filter((message) => message.id < boundary) : messages
|
||||
})
|
||||
const info = createMemo(() => {
|
||||
const id = sessionID()
|
||||
@@ -809,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
|
||||
@@ -1590,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>
|
||||
@@ -1664,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>
|
||||
|
||||
@@ -7,11 +7,11 @@ const assistant = (id: string) => ({ id, role: "assistant" }) as AssistantMessag
|
||||
|
||||
describe("timeline model", () => {
|
||||
test("selects users and applies the revert boundary", () => {
|
||||
const messages: Message[] = [user("msg_z"), assistant("msg_a"), user("msg_b"), user("msg_c")]
|
||||
const messages: Message[] = [user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_5")]
|
||||
const users = selectUserMessages(messages)
|
||||
|
||||
expect(users.map((message) => message.id)).toEqual(["msg_z", "msg_b", "msg_c"])
|
||||
expect(selectVisibleUserMessages(users, "msg_b").map((message) => message.id)).toEqual(["msg_z"])
|
||||
expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"])
|
||||
expect(selectVisibleUserMessages(users, "msg_5").map((message) => message.id)).toEqual(["msg_1", "msg_3"])
|
||||
expect(selectVisibleUserMessages(users)).toBe(users)
|
||||
})
|
||||
|
||||
|
||||
@@ -104,8 +104,7 @@ export function isTimelineReady(messages: Message[] | undefined, loading: boolea
|
||||
|
||||
export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) {
|
||||
if (!revertMessageID) return messages
|
||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||
return boundary < 0 ? messages : messages.slice(0, boundary)
|
||||
return messages.filter((message) => message.id < revertMessageID)
|
||||
}
|
||||
|
||||
export async function loadOlderTimeline(input: {
|
||||
|
||||
@@ -137,11 +137,11 @@ describe("current session timeline rows", () => {
|
||||
|
||||
test("renders an optimistic user turn and thinking before the protocol message arrives", () => {
|
||||
const source = [
|
||||
{ id: "msg_z", type: "user", text: "existing", time: { created: 1 } },
|
||||
{ id: "msg_1", type: "user", text: "existing", time: { created: 1 } },
|
||||
] satisfies SessionMessageInfo[]
|
||||
const normalized = normalizeSessionMessages("ses_1", source)
|
||||
const optimistic = {
|
||||
id: "msg_a",
|
||||
id: "msg_2",
|
||||
sessionID: "ses_1",
|
||||
role: "user" as const,
|
||||
time: { created: 2 },
|
||||
@@ -161,10 +161,10 @@ describe("current session timeline rows", () => {
|
||||
|
||||
expect(result.activeMessageID).toBe(optimistic.id)
|
||||
expect(result.rows.map(TimelineRow.key)).toEqual([
|
||||
"user-message:msg_z",
|
||||
"turn-gap:msg_a",
|
||||
"user-message:msg_a",
|
||||
"thinking:msg_a",
|
||||
"user-message:msg_1",
|
||||
"turn-gap:msg_2",
|
||||
"user-message:msg_2",
|
||||
"thinking:msg_2",
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai
|
||||
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
|
||||
import { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||
import { uniqueSummaryDiffs } from "./summary-diffs"
|
||||
import { compareMessages } from "@/utils/session-message"
|
||||
|
||||
export { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||
|
||||
@@ -72,12 +71,12 @@ export namespace Timeline {
|
||||
turns.push(turn)
|
||||
turnByUserID.set(user.id, turn)
|
||||
})
|
||||
const latestUserMessageID = turns.at(-1)?.user.id
|
||||
projectedUserMessages.forEach((user) => {
|
||||
if (turnByUserID.has(user.id)) return
|
||||
if (latestUserMessageID && user.id < latestUserMessageID) return
|
||||
const turn = { user, assistants: [] }
|
||||
const index = turns.findIndex((item) => compareMessages(user, item.user) < 0)
|
||||
if (index < 0) turns.push(turn)
|
||||
if (index >= 0) turns.splice(index, 0, turn)
|
||||
turns.push(turn)
|
||||
turnByUserID.set(user.id, turn)
|
||||
})
|
||||
const activeMessageID = turns.at(-1)?.user.id
|
||||
|
||||
@@ -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"
|
||||
@@ -100,8 +99,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const visibleUserMessages = () => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
}
|
||||
|
||||
const showAllFiles = () => {
|
||||
@@ -233,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"),
|
||||
@@ -338,9 +311,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const promptSession = prompt.capture()
|
||||
const revert = info()?.revert?.messageID
|
||||
const messages = userMessages()
|
||||
const boundary = revert ? messages.findIndex((message) => message.id === revert) : messages.length
|
||||
if (boundary < 0) return
|
||||
const message = messages[boundary - 1]
|
||||
const message = findLast(messages, (x) => !revert || x.id < revert)
|
||||
if (!message) return
|
||||
const parts = sync().data.part[message.id]
|
||||
|
||||
@@ -355,7 +326,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
updatePrompt: (promptSession) => {
|
||||
if (parts) promptSession.set(extractPromptFromParts(parts, { directory }))
|
||||
},
|
||||
updateViewport: () => setActiveMessage(messages[boundary - 2]),
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < message.id)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -370,16 +341,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
|
||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||
if (boundary < 0) return
|
||||
const next = messages[boundary + 1]
|
||||
const next = messages.find((x) => x.id > revertMessageID)
|
||||
if (!next) {
|
||||
await runCommand({
|
||||
owner,
|
||||
prompt: promptSession,
|
||||
request: () => session.revert.clear({ sessionID }),
|
||||
updatePrompt: (promptSession) => promptSession.reset(),
|
||||
updateViewport: () => setActiveMessage(messages.at(-1)),
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -389,7 +358,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
prompt: promptSession,
|
||||
request: () => session.revert.stage({ sessionID, messageID: next.id }),
|
||||
updatePrompt: () => undefined,
|
||||
updateViewport: () => setActiveMessage(messages[boundary]),
|
||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -489,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)
|
||||
}
|
||||
@@ -12,14 +12,6 @@ const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write
|
||||
const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" }
|
||||
const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
|
||||
export function compareMessages(a: Pick<Message, "id" | "time">, b: Pick<Message, "id" | "time">) {
|
||||
const left = messageKey(a)
|
||||
const right = messageKey(b)
|
||||
return left < right ? -1 : left > right ? 1 : 0
|
||||
}
|
||||
|
||||
export const messageKey = (message: Pick<Message, "id" | "time">) => message.time.created + message.id
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -252,7 +252,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.banner.text": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة",
|
||||
"go.banner.text": "يحصل GPT 5.6 Luna على حدود استخدام مضاعفة لفترة محدودة",
|
||||
"go.meta.description":
|
||||
"يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
|
||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||
|
||||
@@ -256,7 +256,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||
"go.banner.text": "DeepSeek V4 Flash tem limites de uso 2x maiores por tempo limitado",
|
||||
"go.banner.text": "GPT 5.6 Luna tem limites de uso 2x maiores por tempo limitado",
|
||||
"go.meta.description":
|
||||
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
|
||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||
|
||||
@@ -254,7 +254,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||
"go.banner.text": "DeepSeek V4 Flash får fordoblet brugsgrænse i en begrænset periode",
|
||||
"go.banner.text": "GPT 5.6 Luna får fordoblet brugsgrænse i en begrænset periode",
|
||||
"go.meta.description":
|
||||
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
|
||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||
|
||||
@@ -256,7 +256,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||
"go.banner.text": "DeepSeek V4 Flash erhält für begrenzte Zeit 2x Nutzungslimits",
|
||||
"go.banner.text": "GPT 5.6 Luna erhält für begrenzte Zeit 2x Nutzungslimits",
|
||||
"go.meta.description":
|
||||
"Go beginnt bei $5 für deinen ersten Monat, danach $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
|
||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||
"go.banner.text": "DeepSeek V4 Flash gets 2× usage limits for a limited time",
|
||||
"go.banner.text": "GPT 5.6 Luna gets 2× usage limits for a limited time",
|
||||
"go.meta.description":
|
||||
"Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.",
|
||||
"go.hero.title": "Low cost coding models for everyone",
|
||||
|
||||
@@ -257,7 +257,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||
"go.banner.text": "DeepSeek V4 Flash tiene límites de uso 2x mayores por tiempo limitado",
|
||||
"go.banner.text": "GPT 5.6 Luna tiene límites de uso 2x mayores por tiempo limitado",
|
||||
"go.meta.description":
|
||||
"Go comienza en $5 el primer mes, luego 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
|
||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||
|
||||
@@ -258,7 +258,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||
"go.banner.text": "DeepSeek V4 Flash bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
||||
"go.banner.text": "GPT 5.6 Luna bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
||||
"go.meta.description":
|
||||
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
|
||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||
|
||||
@@ -254,7 +254,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||
"go.banner.text": "DeepSeek V4 Flash offre limiti di utilizzo 2x superiori per un periodo limitato",
|
||||
"go.banner.text": "GPT 5.6 Luna offre limiti di utilizzo 2x superiori per un periodo limitato",
|
||||
"go.meta.description":
|
||||
"Go inizia a $5 per il primo mese, poi $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
|
||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||
"go.banner.text": "DeepSeek V4 Flashの利用上限が期間限定で2倍に",
|
||||
"go.banner.text": "GPT 5.6 Lunaの利用上限が期間限定で2倍に",
|
||||
"go.meta.description":
|
||||
"Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
|
||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||
|
||||
@@ -250,7 +250,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||
"go.banner.text": "DeepSeek V4 Flash 사용 한도가 한시적으로 2배 확대됩니다",
|
||||
"go.banner.text": "GPT 5.6 Luna 사용 한도가 한시적으로 2배 확대됩니다",
|
||||
"go.meta.description":
|
||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
|
||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||
|
||||
@@ -254,7 +254,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||
"go.banner.text": "DeepSeek V4 Flash får 2x bruksgrense i en begrenset periode",
|
||||
"go.banner.text": "GPT 5.6 Luna får 2x bruksgrense i en begrenset periode",
|
||||
"go.meta.description":
|
||||
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
|
||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||
|
||||
@@ -255,7 +255,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.banner.text": "DeepSeek V4 Flash oferuje 2x wyższe limity użycia przez ograniczony czas",
|
||||
"go.banner.text": "GPT 5.6 Luna oferuje 2x wyższe limity użycia przez ograniczony czas",
|
||||
"go.meta.description":
|
||||
"Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
|
||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||
|
||||
@@ -258,7 +258,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||
"go.banner.text": "DeepSeek V4 Flash получает 2x лимиты использования на ограниченное время",
|
||||
"go.banner.text": "GPT 5.6 Luna получает 2x лимиты использования на ограниченное время",
|
||||
"go.meta.description":
|
||||
"Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
|
||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||
|
||||
@@ -253,7 +253,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.banner.text": "DeepSeek V4 Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
|
||||
"go.banner.text": "GPT 5.6 Luna เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
|
||||
"go.meta.description":
|
||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
|
||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
|
||||
@@ -256,7 +256,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.banner.text": "DeepSeek V4 Flash sınırlı bir süre için 2x kullanım limiti sunuyor",
|
||||
"go.banner.text": "GPT 5.6 Luna sınırlı bir süre için 2x kullanım limiti sunuyor",
|
||||
"go.meta.description":
|
||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
|
||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||
|
||||
@@ -255,7 +255,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "такими винятками",
|
||||
|
||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||
"go.banner.text": "DeepSeek V4 Flash отримує 2x ліміти використання протягом обмеженого часу",
|
||||
"go.banner.text": "GPT 5.6 Luna отримує 2x ліміти використання протягом обмеженого часу",
|
||||
"go.meta.description":
|
||||
"Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
|
||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||
|
||||
@@ -244,7 +244,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||
"go.banner.text": "DeepSeek V4 Flash 限时享受 2 倍使用额度",
|
||||
"go.banner.text": "GPT 5.6 Luna 限时享受 2 倍使用额度",
|
||||
"go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。",
|
||||
"go.hero.title": "人人可用的低成本编程模型",
|
||||
"go.hero.body":
|
||||
|
||||
@@ -244,7 +244,7 @@ export const dict = {
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||
"go.banner.text": "DeepSeek V4 Flash 限時享有 2 倍使用額度",
|
||||
"go.banner.text": "GPT 5.6 Luna 限時享有 2 倍使用額度",
|
||||
"go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
|
||||
"go.hero.title": "低成本全民編碼模型",
|
||||
"go.hero.body":
|
||||
|
||||
@@ -486,8 +486,8 @@ body {
|
||||
--bar-go: var(--color-go-2);
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
width: calc(100% - 32px);
|
||||
max-width: calc(100% - 32px);
|
||||
width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
[data-slot="plot"] {
|
||||
@@ -496,10 +496,6 @@ body {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
margin-left: -56px;
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="ylabels"] {
|
||||
@@ -529,7 +525,7 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
&:not([data-tick="1"], [data-tick="10"], [data-tick="50"], [data-tick="250"]) {
|
||||
&:not([data-tick="1"], [data-tick="25"], [data-tick="100"], [data-tick="250"]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -576,17 +572,6 @@ body {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
[data-item][data-edge] {
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform: translateY(-50%);
|
||||
max-width: none;
|
||||
padding-left: 16px;
|
||||
background: linear-gradient(90deg, transparent, var(--color-background-weak) 12px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-name] {
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
@@ -604,10 +589,6 @@ body {
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,27 +72,19 @@ function LimitsGraph(props: { href: string }) {
|
||||
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" },
|
||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 4100, baseReq: 2050, d: "290ms" },
|
||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna (2x usage)", req: 4100, baseReq: 2050, d: "290ms" },
|
||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
||||
{ id: "hy3", name: "Hy3", req: 4300, d: "320ms" },
|
||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||
{
|
||||
id: "deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash",
|
||||
req: 63300,
|
||||
baseReq: 31650,
|
||||
edge: true,
|
||||
d: "340ms",
|
||||
},
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
||||
]
|
||||
|
||||
const w = 1040
|
||||
const chartW = 720
|
||||
const w = 720
|
||||
const left = 40
|
||||
const right = 60
|
||||
const top = 18
|
||||
const bottom = 44
|
||||
const plot = chartW - left - right
|
||||
const plot = w - left - right
|
||||
|
||||
const ratio = (n: number) => n / baseline
|
||||
const rmax = Math.max(1, ...graph.map((m) => ratio(m.req)))
|
||||
@@ -203,12 +195,10 @@ function LimitsGraph(props: { href: string }) {
|
||||
data-item
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
data-edge={"edge" in m ? "" : undefined}
|
||||
style={{ "--x": px(x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any}
|
||||
>
|
||||
<span data-value>{m.req.toLocaleString()}</span>
|
||||
<span data-name>{m.name}</span>
|
||||
{m.baseReq && <span data-bonus>2x usage</span>}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -577,32 +577,29 @@ export const filterCompactedEffect = Effect.fnUntraced(function* (sessionID: Ses
|
||||
|
||||
// filterCompacted reorders messages for model consumption
|
||||
// ([compaction-user, summary, ...retained tail..., continue-user]), so array
|
||||
// position is not chronological. IDs are only a deterministic tie-breaker
|
||||
// because imported messages do not necessarily have monotonic IDs.
|
||||
// position is not chronological. Derive each binding by max id (MessageID
|
||||
// is monotonic via MessageID.ascending) so a pre-compaction overflowing tail
|
||||
// assistant doesn't get mistaken for the most recent turn. tasks are
|
||||
// compaction/subtask parts attached to user messages newer than the latest
|
||||
// finished assistant — i.e. unprocessed work.
|
||||
export function latest(msgs: WithParts[]) {
|
||||
let user: User | undefined
|
||||
let assistant: Assistant | undefined
|
||||
let finished: Assistant | undefined
|
||||
for (const msg of msgs) {
|
||||
const info = msg.info
|
||||
if (info.role === "user" && isAfter(info, user)) user = info
|
||||
if (info.role === "assistant" && isAfter(info, assistant)) assistant = info
|
||||
if (info.role === "assistant" && info.finish && isAfter(info, finished)) finished = info
|
||||
if (info.role === "user" && (!user || info.id > user.id)) user = info
|
||||
if (info.role === "assistant" && (!assistant || info.id > assistant.id)) assistant = info
|
||||
if (info.role === "assistant" && info.finish && (!finished || info.id > finished.id)) finished = info
|
||||
}
|
||||
const tasks = msgs.flatMap((m) =>
|
||||
finished && !isAfter(m.info, finished)
|
||||
finished && m.info.id <= finished.id
|
||||
? []
|
||||
: m.parts.filter((p): p is CompactionPart | SubtaskPart => p.type === "compaction" || p.type === "subtask"),
|
||||
)
|
||||
return { user, assistant, finished, tasks }
|
||||
}
|
||||
|
||||
function isAfter(info: Info, other?: Info) {
|
||||
if (!other) return true
|
||||
if (info.time.created !== other.time.created) return info.time.created > other.time.created
|
||||
return info.id > other.id
|
||||
}
|
||||
|
||||
export function fromError(
|
||||
e: unknown,
|
||||
ctx: { providerID: ProviderV2.ID; aborted?: boolean },
|
||||
|
||||
@@ -1112,7 +1112,7 @@ const layer = Layer.effect(
|
||||
lastAssistant?.finish &&
|
||||
!["tool-calls"].includes(lastAssistant.finish) &&
|
||||
!hasToolCalls &&
|
||||
lastAssistant.parentID === lastUser.id
|
||||
lastUser.id < lastAssistant.id
|
||||
) {
|
||||
const orphan = lastAssistantMsg?.parts.find(
|
||||
(part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
|
||||
|
||||
@@ -71,8 +71,7 @@ const layer = Layer.effect(
|
||||
if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot)
|
||||
yield* snap.revert(patches)
|
||||
if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot)
|
||||
const index = all.findIndex((msg) => msg.info.id === rev.messageID)
|
||||
const range = index < 0 ? [] : all.slice(index)
|
||||
const range = all.filter((msg) => msg.info.id >= rev.messageID)
|
||||
const diffs = yield* summary.computeDiff({ messages: range })
|
||||
yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
|
||||
yield* events.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
|
||||
@@ -103,9 +102,20 @@ const layer = Layer.effect(
|
||||
const sessionID = session.id
|
||||
const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie)
|
||||
const messageID = session.revert.messageID
|
||||
const index = msgs.findIndex((msg) => msg.info.id === messageID)
|
||||
const target = index < 0 ? undefined : msgs[index]
|
||||
const remove = index < 0 ? [] : msgs.slice(index + (session.revert.partID ? 1 : 0))
|
||||
const remove = [] as SessionV1.WithParts[]
|
||||
let target: SessionV1.WithParts | undefined
|
||||
for (const msg of msgs) {
|
||||
if (msg.info.id < messageID) continue
|
||||
if (msg.info.id > messageID) {
|
||||
remove.push(msg)
|
||||
continue
|
||||
}
|
||||
if (session.revert.partID) {
|
||||
target = msg
|
||||
continue
|
||||
}
|
||||
remove.push(msg)
|
||||
}
|
||||
for (const msg of remove) {
|
||||
yield* sessions.removeMessage({ sessionID, messageID: msg.info.id })
|
||||
}
|
||||
|
||||
@@ -703,9 +703,9 @@ const layer: Layer.Layer<
|
||||
})
|
||||
const msgs = yield* messages({ sessionID: input.sessionID })
|
||||
const idMap = new Map<string, MessageID>()
|
||||
const target = input.messageID ? msgs.findIndex((msg) => msg.info.id === input.messageID) : msgs.length
|
||||
|
||||
for (const msg of msgs.slice(0, target < 0 ? msgs.length : target)) {
|
||||
for (const msg of msgs) {
|
||||
if (input.messageID && msg.info.id >= input.messageID) break
|
||||
const newID = MessageID.ascending()
|
||||
idMap.set(msg.info.id, newID)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Agent } from "../agent/agent"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { evaluate } from "@/permission/evaluate"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "../id/id"
|
||||
import { ToolID } from "./schema"
|
||||
import { TRUNCATION_DIR } from "./truncation-dir"
|
||||
|
||||
@@ -51,17 +52,16 @@ const layer = Layer.effect(
|
||||
const fs = yield* FSUtil.Service
|
||||
|
||||
const cleanup = Effect.fn("Truncate.cleanup")(function* () {
|
||||
const cutoff = Date.now() - Duration.toMillis(RETENTION)
|
||||
const cutoff = Identifier.timestamp(
|
||||
Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)),
|
||||
)
|
||||
const entries = yield* fs.readDirectory(TRUNCATION_DIR).pipe(
|
||||
Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
|
||||
Effect.catch(() => Effect.succeed([])),
|
||||
)
|
||||
for (const entry of entries) {
|
||||
const file = path.join(TRUNCATION_DIR, entry)
|
||||
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
const mtime = info && Option.getOrUndefined(info.mtime)
|
||||
if (!mtime || mtime.getTime() >= cutoff) continue
|
||||
yield* fs.remove(file).pipe(Effect.catch(() => Effect.void))
|
||||
if (Identifier.timestamp(entry) >= cutoff) continue
|
||||
yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -1611,44 +1611,6 @@ describe("session.message-v2.latest", () => {
|
||||
] as SessionV1.Part[],
|
||||
}
|
||||
|
||||
test("selects latest messages by creation time when IDs are nonmonotonic", () => {
|
||||
const oldUser = { ...userInfo("msg_z_user"), time: { created: 100 } }
|
||||
const newUser = { ...userInfo("msg_a_user"), time: { created: 200 } }
|
||||
const oldAssistant = {
|
||||
...assistantInfo("msg_z_assistant", oldUser.id),
|
||||
time: { created: 300 },
|
||||
finish: "stop",
|
||||
} as SessionV1.Assistant
|
||||
const newAssistant = {
|
||||
...assistantInfo("msg_a_assistant", newUser.id),
|
||||
time: { created: 400 },
|
||||
finish: "stop",
|
||||
} as SessionV1.Assistant
|
||||
|
||||
const state = MessageV2.latest([
|
||||
{ info: newAssistant, parts: [] },
|
||||
{ info: oldUser, parts: [] },
|
||||
{ info: oldAssistant, parts: [] },
|
||||
{ info: newUser, parts: [] },
|
||||
])
|
||||
|
||||
expect(state.user?.id).toBe(newUser.id)
|
||||
expect(state.assistant?.id).toBe(newAssistant.id)
|
||||
expect(state.finished?.id).toBe(newAssistant.id)
|
||||
})
|
||||
|
||||
test("uses ID as a deterministic tie-breaker for equal creation times", () => {
|
||||
const lower = { ...userInfo("msg_a_user"), time: { created: 100 } }
|
||||
const higher = { ...userInfo("msg_z_user"), time: { created: 100 } }
|
||||
|
||||
const state = MessageV2.latest([
|
||||
{ info: higher, parts: [] },
|
||||
{ info: lower, parts: [] },
|
||||
])
|
||||
|
||||
expect(state.user?.id).toBe(higher.id)
|
||||
})
|
||||
|
||||
// Regression for double auto-compaction. The reorder in filterCompacted
|
||||
// (#27145) returns [compaction-user, summary, ...tail..., continue-user],
|
||||
// so picking lastFinished by array position landed on the pre-compaction
|
||||
@@ -1697,33 +1659,4 @@ describe("session.message-v2.latest", () => {
|
||||
expect(state.tasks).toHaveLength(1)
|
||||
expect(state.tasks[0]).toMatchObject({ type: "compaction", auto: true })
|
||||
})
|
||||
|
||||
test("selects compaction and subtask work after the finished boundary by creation time", () => {
|
||||
const finished = {
|
||||
...assistantInfo("msg_z_finished", "msg_parent"),
|
||||
time: { created: 200 },
|
||||
finish: "stop",
|
||||
} as SessionV1.Assistant
|
||||
const oldTask: SessionV1.WithParts = {
|
||||
info: { ...userInfo("msg_z_old"), time: { created: 100 } },
|
||||
parts: [{ ...basePart("msg_z_old", "old"), type: "compaction", auto: true }] as SessionV1.Part[],
|
||||
}
|
||||
const newTask: SessionV1.WithParts = {
|
||||
info: { ...userInfo("msg_a_new"), time: { created: 300 } },
|
||||
parts: [
|
||||
{
|
||||
...basePart("msg_a_new", "new"),
|
||||
type: "subtask",
|
||||
prompt: "inspect",
|
||||
description: "inspect ordering",
|
||||
agent: "general",
|
||||
},
|
||||
] as SessionV1.Part[],
|
||||
}
|
||||
|
||||
const state = MessageV2.latest([newTask, { info: finished, parts: [] }, oldTask])
|
||||
|
||||
expect(state.tasks).toHaveLength(1)
|
||||
expect(state.tasks[0]).toMatchObject({ type: "subtask", prompt: "inspect" })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -610,7 +610,9 @@ it.live("session.processor effect tests retry OpenAI-compatible midstream server
|
||||
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.push(
|
||||
raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }),
|
||||
)
|
||||
yield* llm.text("after")
|
||||
|
||||
const chat = yield* session.create({})
|
||||
|
||||
@@ -460,46 +460,6 @@ noLLMServer.instance(
|
||||
{ config: cfg },
|
||||
)
|
||||
|
||||
noLLMServer.instance(
|
||||
"loop exits for a completed parent turn with nonmonotonic message IDs",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "Pinned" })
|
||||
const userID = MessageID.make("msg_z_user")
|
||||
const assistantID = MessageID.make("msg_a_assistant")
|
||||
yield* sessions.updateMessage({
|
||||
id: userID,
|
||||
role: "user",
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
time: { created: 100 },
|
||||
})
|
||||
yield* sessions.updateMessage({
|
||||
id: assistantID,
|
||||
role: "assistant",
|
||||
parentID: userID,
|
||||
sessionID: chat.id,
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
cost: 0,
|
||||
path: { cwd: "/tmp", root: "/tmp" },
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
modelID: ref.modelID,
|
||||
providerID: ref.providerID,
|
||||
time: { created: 200, completed: 201 },
|
||||
finish: "stop",
|
||||
})
|
||||
|
||||
const result = yield* prompt.loop({ sessionID: chat.id })
|
||||
|
||||
expect(result.info.id).toBe(assistantID)
|
||||
}),
|
||||
{ config: cfg },
|
||||
)
|
||||
|
||||
it.instance("loop exits without an LLM request for interrupted orphan tool calls", () =>
|
||||
Effect.gen(function* () {
|
||||
const { llm } = yield* useServerConfig(providerCfg)
|
||||
|
||||
@@ -35,18 +35,6 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "de
|
||||
})
|
||||
})
|
||||
|
||||
const userAt = Effect.fn("test.userAt")(function* (sessionID: SessionID, id: string, created: number) {
|
||||
const session = yield* Session.Service
|
||||
return yield* session.updateMessage({
|
||||
id: MessageID.make(id),
|
||||
role: "user" as const,
|
||||
sessionID,
|
||||
agent: "default",
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: ModelV2.ID.make("gpt-4") },
|
||||
time: { created },
|
||||
})
|
||||
})
|
||||
|
||||
const assistant = Effect.fn("test.assistant")(function* (sessionID: SessionID, parentID: MessageID, dir: string) {
|
||||
const session = yield* Session.Service
|
||||
return yield* session.updateMessage({
|
||||
@@ -438,39 +426,6 @@ describe("revert + compact workflow", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"reverts chronological suffixes on both sides of mixed message ID ordering",
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const revert = yield* SessionRevert.Service
|
||||
const ids = ["msg_z9-before", "msg_z1-before-wrap", "msg_a0-after-wrap", "msg_a1-after"]
|
||||
|
||||
const run = Effect.fn("test.mixedIDRevert")(function* (target: number) {
|
||||
const info = yield* session.create({})
|
||||
for (const [index, id] of ids.entries()) {
|
||||
const message = yield* userAt(info.id, id, index + 1)
|
||||
yield* text(info.id, message.id, id)
|
||||
}
|
||||
|
||||
const reverted = yield* revert.revert({
|
||||
sessionID: info.id,
|
||||
messageID: MessageID.make(ids[target]!),
|
||||
})
|
||||
yield* revert.cleanup(reverted)
|
||||
const remaining = yield* session.messages({ sessionID: info.id })
|
||||
yield* session.remove(info.id)
|
||||
return remaining.map((msg) => msg.info.time.created)
|
||||
})
|
||||
|
||||
expect(yield* run(1)).toEqual([1])
|
||||
expect(yield* run(2)).toEqual([1, 2])
|
||||
}),
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cleanup is a no-op when session has no revert state",
|
||||
provideTmpdirInstance(
|
||||
|
||||
@@ -238,38 +238,6 @@ describe("Session", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("forks the chronological prefix across mixed message ID ordering", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionNs.Service
|
||||
const created = yield* Effect.acquireRelease(session.create({}), (info) =>
|
||||
session.remove(info.id).pipe(Effect.ignore),
|
||||
)
|
||||
const ids = ["msg_z9-before", "msg_z1-before-wrap", "msg_a0-after-wrap", "msg_a1-after"]
|
||||
for (const [index, id] of ids.entries()) {
|
||||
yield* session.updateMessage({
|
||||
id: MessageID.make(id),
|
||||
sessionID: created.id,
|
||||
role: "user",
|
||||
time: { created: index + 1 },
|
||||
agent: "user",
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
} as SessionV1.User)
|
||||
}
|
||||
|
||||
const beforeWrap = yield* Effect.acquireRelease(
|
||||
session.fork({ sessionID: created.id, messageID: MessageID.make(ids[1]!) }),
|
||||
(info) => session.remove(info.id).pipe(Effect.ignore),
|
||||
)
|
||||
const afterWrap = yield* Effect.acquireRelease(
|
||||
session.fork({ sessionID: created.id, messageID: MessageID.make(ids[2]!) }),
|
||||
(info) => session.remove(info.id).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
expect((yield* session.messages({ sessionID: beforeWrap.id })).map((msg) => msg.info.time.created)).toEqual([1])
|
||||
expect((yield* session.messages({ sessionID: afterWrap.id })).map((msg) => msg.info.time.created)).toEqual([1, 2])
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("omits metadata when not provided", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionNs.Service
|
||||
|
||||
@@ -242,20 +242,18 @@ describe("Truncate", () => {
|
||||
describe("cleanup", () => {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
it.live("uses file mtime when IDs wrap", () =>
|
||||
it.live("deletes files older than 7 days and preserves recent files", () =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Truncate.Service
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
|
||||
yield* fs.makeDirectory(Truncate.DIR, { recursive: true })
|
||||
|
||||
const old = path.join(Truncate.DIR, Identifier.create("tool", "ascending", 2 ** 36 - 1))
|
||||
const recent = path.join(Truncate.DIR, Identifier.create("tool", "ascending", 2 ** 36 + 1))
|
||||
const old = path.join(Truncate.DIR, Identifier.create("tool", "ascending", Date.now() - 10 * DAY_MS))
|
||||
const recent = path.join(Truncate.DIR, Identifier.create("tool", "ascending", Date.now() - 3 * DAY_MS))
|
||||
|
||||
yield* writeFileStringScoped(old, "old content")
|
||||
yield* writeFileStringScoped(recent, "recent content")
|
||||
yield* fs.utimes(old, new Date(), new Date(Date.now() - 10 * DAY_MS))
|
||||
yield* fs.utimes(recent, new Date(), new Date(Date.now() - 3 * DAY_MS))
|
||||
yield* svc.cleanup()
|
||||
|
||||
expect(yield* fs.exists(old)).toBe(false)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user