mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bff28de09 | |||
| 38e10eb140 | |||
| fe82a1b6ca | |||
| 2ea728f731 | |||
| 284214c78d | |||
| b05a0d1fbe | |||
| be25c905fa | |||
| 5347b5e009 | |||
| b467518dae | |||
| 1ec6bdc8c6 | |||
| 265472ef72 | |||
| 325529761b | |||
| 741244b69d | |||
| 4abddbbfdd | |||
| 5aa5cb3523 | |||
| 9113255114 | |||
| 23cc677108 | |||
| 28bcc0e4f4 | |||
| 8bf5062b89 | |||
| 20750c332e | |||
| db581e47a3 | |||
| a54a693af2 | |||
| d468201952 |
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"lildax": "./bin/lildax.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -267,7 +267,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
@@ -435,7 +435,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -449,7 +449,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -461,7 +461,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -493,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -509,7 +509,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -690,7 +690,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -766,7 +766,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -781,7 +781,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -796,7 +796,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||||
@@ -836,7 +836,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -849,7 +849,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -883,7 +883,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -902,7 +902,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -944,7 +944,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
@@ -971,7 +971,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1022,7 +1022,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
- 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.
|
- 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.
|
- 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`.
|
- 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`.
|
||||||
|
- For developer-facing terminology, prefer the words already used by the target language's developer community over literal dictionary translations. Cross-check maintained localized developer products such as Firefox, KDE, and VS Code; use at least two independent corpora when they are available. If established practice keeps an English loanword or acronym, keep it rather than inventing a translation.
|
||||||
|
- Translate complete UI phrases in context. A glossary hit is evidence, not permission to translate word-by-word. Check terse labels such as session, prompt, agent, model, fork, shell, terminal, workspace, and worktree in the same grammatical role before choosing a term.
|
||||||
|
- Before a locale is ready, audit recurring concepts for one consistent translation and review every value that still equals English. Classify retained English as a product name, provider/tool name, URL, code token, keyboard legend, acronym, asset name, or established borrowing; translate unexplained leftovers.
|
||||||
|
- In translation review notes, name the corpora used and call out uncertain or region-specific terminology so native speakers can focus review where it matters.
|
||||||
- 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.
|
- 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.
|
||||||
|
|
||||||
## Tool Calling
|
## Tool Calling
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ test("resolves directory autocomplete from the current browser root", async () =
|
|||||||
directories.push(input.location?.directory ?? "")
|
directories.push(input.location?.directory ?? "")
|
||||||
return Promise.resolve({ data: [] })
|
return Promise.resolve({ data: [] })
|
||||||
},
|
},
|
||||||
|
list: () => Promise.resolve({ data: [] }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||||
@@ -152,6 +153,67 @@ test("resolves directory autocomplete from the current browser root", async () =
|
|||||||
expect(directories).toEqual(["/repo", "/repo/src"])
|
expect(directories).toEqual(["/repo", "/repo/src"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps indexed directory results for servers that support empty search", async () => {
|
||||||
|
const sdk = {
|
||||||
|
api: {
|
||||||
|
file: {
|
||||||
|
find: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
|
||||||
|
list: () => Promise.reject(new Error("listing should not run when search returns results")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||||
|
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||||
|
|
||||||
|
expect(await search("")).toEqual(["/home/luke/projects"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("lists the default directory when empty search is unsupported", async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const directories = Array.from({ length: 60 }, (_, index) => ({
|
||||||
|
path: `project-${index}/`,
|
||||||
|
type: "directory" as const,
|
||||||
|
}))
|
||||||
|
const sdk = {
|
||||||
|
api: {
|
||||||
|
file: {
|
||||||
|
find: () => Promise.resolve({ data: [] }),
|
||||||
|
list: (input: { location?: { directory?: string } }) => {
|
||||||
|
calls.push(input.location?.directory ?? "")
|
||||||
|
return Promise.resolve({
|
||||||
|
data: [...directories, { path: "README.md", type: "file" }],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||||
|
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||||
|
|
||||||
|
const results = await search("")
|
||||||
|
expect(results).toHaveLength(60)
|
||||||
|
expect(results.at(-1)).toBe("/home/luke/project-59")
|
||||||
|
expect(calls).toEqual(["/home/luke"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("matches the default directory listing when typed search is unsupported", async () => {
|
||||||
|
const sdk = {
|
||||||
|
api: {
|
||||||
|
file: {
|
||||||
|
find: () => Promise.resolve({ data: [] }),
|
||||||
|
list: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: [
|
||||||
|
{ path: "Documents/", type: "directory" },
|
||||||
|
{ path: "Downloads/", type: "directory" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||||
|
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||||
|
|
||||||
|
expect(await search("documents")).toEqual(["/home/luke/Documents"])
|
||||||
|
})
|
||||||
|
|
||||||
test("searches from an absolute root without a default base", async () => {
|
test("searches from an absolute root without a default base", async () => {
|
||||||
const directories: string[] = []
|
const directories: string[] = []
|
||||||
const sdk = {
|
const sdk = {
|
||||||
|
|||||||
@@ -379,7 +379,14 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
|||||||
.then((result) => result.data.map((entry) => entry.path))
|
.then((result) => result.data.map((entry) => entry.path))
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
if (!active()) return []
|
if (!active()) return []
|
||||||
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
if (results.length) {
|
||||||
|
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
||||||
|
}
|
||||||
|
const fallback = query
|
||||||
|
? await match(input.directory, query, 50)
|
||||||
|
: (await directories(input.directory)).map((item) => item.absolute)
|
||||||
|
if (!active()) return []
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
const segments = query.replace(/^\/+/, "").split("/")
|
const segments = query.replace(/^\/+/, "").split("/")
|
||||||
const head = segments.slice(0, -1).filter((part) => part && part !== ".")
|
const head = segments.slice(0, -1).filter((part) => part && part !== ".")
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ export function SessionContextTab() {
|
|||||||
() => {
|
() => {
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
if (!revert) return userMessages()
|
if (!revert) return userMessages()
|
||||||
return userMessages().filter((m) => m.id < revert)
|
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||||
|
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||||
},
|
},
|
||||||
emptyUserMessages,
|
emptyUserMessages,
|
||||||
{ equals: same },
|
{ equals: same },
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ const rootSession = (input: { id: string; parentID?: string; archived?: number }
|
|||||||
},
|
},
|
||||||
}) as Session
|
}) as Session
|
||||||
|
|
||||||
const userMessage = (id: string, sessionID: string) =>
|
const userMessage = (id: string, sessionID: string, created = 1) =>
|
||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: { created: 1 },
|
time: { created },
|
||||||
agent: "assistant",
|
agent: "assistant",
|
||||||
model: { providerID: "openai", modelID: "gpt" },
|
model: { providerID: "openai", modelID: "gpt" },
|
||||||
}) as Message
|
}) as Message
|
||||||
@@ -370,13 +370,13 @@ describe("applyDirectoryEvent", () => {
|
|||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const [store, setStore] = createStore(
|
const [store, setStore] = createStore(
|
||||||
baseState({
|
baseState({
|
||||||
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
|
message: { [sessionID]: [userMessage("msg_z", sessionID, 1), userMessage("msg_b", sessionID, 3)] },
|
||||||
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
|
part: { msg_a: [textPart("prt_1", sessionID, "msg_a")] },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
|
event: { type: "message.updated", properties: { info: userMessage("msg_a", sessionID, 2) } },
|
||||||
store,
|
store,
|
||||||
setStore,
|
setStore,
|
||||||
push() {},
|
push() {},
|
||||||
@@ -384,14 +384,14 @@ describe("applyDirectoryEvent", () => {
|
|||||||
loadLsp() {},
|
loadLsp() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a", "msg_b"])
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: {
|
event: {
|
||||||
type: "message.updated",
|
type: "message.updated",
|
||||||
properties: {
|
properties: {
|
||||||
info: {
|
info: {
|
||||||
...userMessage("msg_2", sessionID),
|
...userMessage("msg_a", sessionID, 2),
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
} as Message,
|
} as Message,
|
||||||
},
|
},
|
||||||
@@ -403,10 +403,10 @@ describe("applyDirectoryEvent", () => {
|
|||||||
loadLsp() {},
|
loadLsp() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
|
expect(store.message[sessionID]?.find((x) => x.id === "msg_a")?.role).toBe("assistant")
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
|
event: { type: "message.removed", properties: { sessionID, messageID: "msg_a" } },
|
||||||
store,
|
store,
|
||||||
setStore,
|
setStore,
|
||||||
push() {},
|
push() {},
|
||||||
@@ -414,8 +414,8 @@ describe("applyDirectoryEvent", () => {
|
|||||||
loadLsp() {},
|
loadLsp() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_b"])
|
||||||
expect(store.part.msg_2).toBeUndefined()
|
expect(store.part.msg_a).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("upserts and prunes message parts", () => {
|
test("upserts and prunes message parts", () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { State, VcsCache } from "./types"
|
|||||||
import { trimSessions } from "./session-trim"
|
import { trimSessions } from "./session-trim"
|
||||||
import { dropSessionCaches } from "./session-cache"
|
import { dropSessionCaches } from "./session-cache"
|
||||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
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 SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
const SESSION_CONTENT_EVENTS = new Set([
|
const SESSION_CONTENT_EVENTS = new Set([
|
||||||
@@ -275,7 +276,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
input.setStore("message", info.sessionID, [info])
|
input.setStore("message", info.sessionID, [info])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const result = Binary.search(messages, info.id, (m) => m.id)
|
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setStore("message", info.sessionID, result.index, reconcile(info))
|
input.setStore("message", info.sessionID, result.index, reconcile(info))
|
||||||
break
|
break
|
||||||
@@ -295,8 +296,8 @@ export function applyDirectoryEvent(input: {
|
|||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[props.sessionID]
|
const messages = draft.message[props.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (index >= 0) messages.splice(index, 1)
|
||||||
}
|
}
|
||||||
const parts = draft.part[props.messageID]
|
const parts = draft.part[props.messageID]
|
||||||
if (parts) {
|
if (parts) {
|
||||||
@@ -322,7 +323,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
input.setStore("part", part.messageID, [part])
|
input.setStore("part", part.messageID, [part])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const result = Binary.search(parts, part.id, (p) => p.id)
|
const result = Binary.search(parts, part.id, (item) => item.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setStore("part", part.messageID, result.index, reconcile(part))
|
input.setStore("part", part.messageID, result.index, reconcile(part))
|
||||||
break
|
break
|
||||||
@@ -345,13 +346,13 @@ export function applyDirectoryEvent(input: {
|
|||||||
)
|
)
|
||||||
const parts = input.store.part[props.messageID]
|
const parts = input.store.part[props.messageID]
|
||||||
if (!parts) break
|
if (!parts) break
|
||||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
if (result.found) {
|
if (result.found) {
|
||||||
input.setStore(
|
input.setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const list = draft.part[props.messageID]
|
const list = draft.part[props.messageID]
|
||||||
if (!list) return
|
if (!list) return
|
||||||
const next = Binary.search(list, props.partID, (p) => p.id)
|
const next = Binary.search(list, props.partID, (part) => part.id)
|
||||||
if (!next.found) return
|
if (!next.found) return
|
||||||
list.splice(next.index, 1)
|
list.splice(next.index, 1)
|
||||||
if (list.length === 0) delete draft.part[props.messageID]
|
if (list.length === 0) delete draft.part[props.messageID]
|
||||||
@@ -364,7 +365,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||||
const parts = input.store.part[props.messageID]
|
const parts = input.store.part[props.messageID]
|
||||||
if (!parts) break
|
if (!parts) break
|
||||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||||
if (!result.found) break
|
if (!result.found) break
|
||||||
const field = props.field as keyof (typeof parts)[number]
|
const field = props.field as keyof (typeof parts)[number]
|
||||||
const current = parts[result.index]?.[field]
|
const current = parts[result.index]?.[field]
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { dict as en } from "@/i18n/en"
|
|||||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||||
import {
|
import {
|
||||||
createDesktopNativeBundle,
|
createDesktopNativeBundle,
|
||||||
|
detectDesktopNativeLocale,
|
||||||
DESKTOP_NATIVE_ENGLISH,
|
DESKTOP_NATIVE_ENGLISH,
|
||||||
DESKTOP_NATIVE_LABELS,
|
DESKTOP_NATIVE_LABELS,
|
||||||
DESKTOP_NATIVE_LOCALES,
|
DESKTOP_NATIVE_LOCALES,
|
||||||
|
DESKTOP_NATIVE_LOCALE_TAGS,
|
||||||
type DesktopNativeBundle,
|
type DesktopNativeBundle,
|
||||||
type DesktopNativeLocale,
|
type DesktopNativeLocale,
|
||||||
} from "@/i18n/desktop-native"
|
} from "@/i18n/desktop-native"
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
export type Locale = DesktopNativeLocale
|
export type Locale = DesktopNativeLocale
|
||||||
export type Direction = "ltr" | "rtl"
|
export type Direction = "ltr" | "rtl"
|
||||||
|
|
||||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"])
|
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"])
|
||||||
|
|
||||||
function localeDirection(locale: Locale): Direction {
|
function localeDirection(locale: Locale): Direction {
|
||||||
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||||
@@ -39,36 +41,7 @@ function cookie(locale: Locale) {
|
|||||||
|
|
||||||
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
|
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
|
||||||
|
|
||||||
const INTL: Record<Locale, string> = {
|
const INTL = DESKTOP_NATIVE_LOCALE_TAGS
|
||||||
en: "en",
|
|
||||||
zh: "zh-Hans",
|
|
||||||
zht: "zh-Hant",
|
|
||||||
ko: "ko",
|
|
||||||
de: "de",
|
|
||||||
es: "es",
|
|
||||||
fr: "fr",
|
|
||||||
da: "da",
|
|
||||||
ja: "ja",
|
|
||||||
pl: "pl",
|
|
||||||
ru: "ru",
|
|
||||||
uk: "uk",
|
|
||||||
ar: "ar",
|
|
||||||
no: "nb-NO",
|
|
||||||
br: "pt-BR",
|
|
||||||
th: "th",
|
|
||||||
bs: "bs",
|
|
||||||
tr: "tr",
|
|
||||||
hi: "hi-IN",
|
|
||||||
nl: "nl-NL",
|
|
||||||
id: "id-ID",
|
|
||||||
vi: "vi-VN",
|
|
||||||
it: "it-IT",
|
|
||||||
ur: "ur-PK",
|
|
||||||
pa: "pa-Arab-PK",
|
|
||||||
az: "az-Latn-AZ",
|
|
||||||
fi: "fi-FI",
|
|
||||||
sv: "sv-SE",
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = i18n.flatten({ ...en, ...uiEn })
|
const base = i18n.flatten({ ...en, ...uiEn })
|
||||||
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
||||||
@@ -104,6 +77,40 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
|||||||
az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")),
|
az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")),
|
||||||
fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")),
|
fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")),
|
||||||
sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")),
|
sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")),
|
||||||
|
am: () => merge(import("@/i18n/am"), import("@opencode-ai/ui/i18n/am")),
|
||||||
|
bg: () => merge(import("@/i18n/bg"), import("@opencode-ai/ui/i18n/bg")),
|
||||||
|
bn: () => merge(import("@/i18n/bn"), import("@opencode-ai/ui/i18n/bn")),
|
||||||
|
ca: () => merge(import("@/i18n/ca"), import("@opencode-ai/ui/i18n/ca")),
|
||||||
|
cs: () => merge(import("@/i18n/cs"), import("@opencode-ai/ui/i18n/cs")),
|
||||||
|
dv: () => merge(import("@/i18n/dv"), import("@opencode-ai/ui/i18n/dv")),
|
||||||
|
dz: () => merge(import("@/i18n/dz"), import("@opencode-ai/ui/i18n/dz")),
|
||||||
|
el: () => merge(import("@/i18n/el"), import("@opencode-ai/ui/i18n/el")),
|
||||||
|
et: () => merge(import("@/i18n/et"), import("@opencode-ai/ui/i18n/et")),
|
||||||
|
fa: () => merge(import("@/i18n/fa"), import("@opencode-ai/ui/i18n/fa")),
|
||||||
|
fo: () => merge(import("@/i18n/fo"), import("@opencode-ai/ui/i18n/fo")),
|
||||||
|
hr: () => merge(import("@/i18n/hr"), import("@opencode-ai/ui/i18n/hr")),
|
||||||
|
hu: () => merge(import("@/i18n/hu"), import("@opencode-ai/ui/i18n/hu")),
|
||||||
|
hy: () => merge(import("@/i18n/hy"), import("@opencode-ai/ui/i18n/hy")),
|
||||||
|
is: () => merge(import("@/i18n/is"), import("@opencode-ai/ui/i18n/is")),
|
||||||
|
ka: () => merge(import("@/i18n/ka"), import("@opencode-ai/ui/i18n/ka")),
|
||||||
|
km: () => merge(import("@/i18n/km"), import("@opencode-ai/ui/i18n/km")),
|
||||||
|
lo: () => merge(import("@/i18n/lo"), import("@opencode-ai/ui/i18n/lo")),
|
||||||
|
lt: () => merge(import("@/i18n/lt"), import("@opencode-ai/ui/i18n/lt")),
|
||||||
|
lv: () => merge(import("@/i18n/lv"), import("@opencode-ai/ui/i18n/lv")),
|
||||||
|
mk: () => merge(import("@/i18n/mk"), import("@opencode-ai/ui/i18n/mk")),
|
||||||
|
mn: () => merge(import("@/i18n/mn"), import("@opencode-ai/ui/i18n/mn")),
|
||||||
|
ms: () => merge(import("@/i18n/ms"), import("@opencode-ai/ui/i18n/ms")),
|
||||||
|
my: () => merge(import("@/i18n/my"), import("@opencode-ai/ui/i18n/my")),
|
||||||
|
ne: () => merge(import("@/i18n/ne"), import("@opencode-ai/ui/i18n/ne")),
|
||||||
|
ro: () => merge(import("@/i18n/ro"), import("@opencode-ai/ui/i18n/ro")),
|
||||||
|
si: () => merge(import("@/i18n/si"), import("@opencode-ai/ui/i18n/si")),
|
||||||
|
sk: () => merge(import("@/i18n/sk"), import("@opencode-ai/ui/i18n/sk")),
|
||||||
|
sl: () => merge(import("@/i18n/sl"), import("@opencode-ai/ui/i18n/sl")),
|
||||||
|
sq: () => merge(import("@/i18n/sq"), import("@opencode-ai/ui/i18n/sq")),
|
||||||
|
sr: () => merge(import("@/i18n/sr"), import("@opencode-ai/ui/i18n/sr")),
|
||||||
|
tg: () => merge(import("@/i18n/tg"), import("@opencode-ai/ui/i18n/tg")),
|
||||||
|
tk: () => merge(import("@/i18n/tk"), import("@opencode-ai/ui/i18n/tk")),
|
||||||
|
uz: () => merge(import("@/i18n/uz"), import("@opencode-ai/ui/i18n/uz")),
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDict(locale: Locale) {
|
function loadDict(locale: Locale) {
|
||||||
@@ -121,63 +128,9 @@ export function loadLocaleDict(locale: Locale) {
|
|||||||
return loadDict(locale).then(() => undefined)
|
return loadDict(locale).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
|
||||||
{ locale: "en", match: (language) => language.startsWith("en") },
|
|
||||||
{
|
|
||||||
locale: "zht",
|
|
||||||
match: (language) =>
|
|
||||||
language.startsWith("zh") &&
|
|
||||||
(language.includes("hant") || language.includes("-tw") || language.includes("-hk") || language.includes("-mo")),
|
|
||||||
},
|
|
||||||
{ locale: "zh", match: (language) => language.startsWith("zh") },
|
|
||||||
{ locale: "ko", match: (language) => language.startsWith("ko") },
|
|
||||||
{ locale: "de", match: (language) => language.startsWith("de") },
|
|
||||||
{ locale: "es", match: (language) => language.startsWith("es") },
|
|
||||||
{ locale: "fr", match: (language) => language.startsWith("fr") },
|
|
||||||
{ locale: "da", match: (language) => language.startsWith("da") },
|
|
||||||
{ locale: "ja", match: (language) => language.startsWith("ja") },
|
|
||||||
{ locale: "pl", match: (language) => language.startsWith("pl") },
|
|
||||||
{ locale: "ru", match: (language) => language.startsWith("ru") },
|
|
||||||
{ locale: "uk", match: (language) => language.startsWith("uk") },
|
|
||||||
{ locale: "ar", match: (language) => language.startsWith("ar") },
|
|
||||||
{
|
|
||||||
locale: "no",
|
|
||||||
match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"),
|
|
||||||
},
|
|
||||||
{ locale: "br", match: (language) => language.startsWith("pt") },
|
|
||||||
{ locale: "th", match: (language) => language.startsWith("th") },
|
|
||||||
{ locale: "bs", match: (language) => language.startsWith("bs") },
|
|
||||||
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
|
||||||
{ locale: "hi", match: (language) => language.startsWith("hi") },
|
|
||||||
{ locale: "nl", match: (language) => language.startsWith("nl") },
|
|
||||||
{ locale: "id", match: (language) => language.startsWith("id") },
|
|
||||||
{ locale: "vi", match: (language) => language.startsWith("vi") },
|
|
||||||
{ locale: "it", match: (language) => language.startsWith("it") },
|
|
||||||
{ locale: "ur", match: (language) => language.startsWith("ur") },
|
|
||||||
{
|
|
||||||
locale: "pa",
|
|
||||||
match: (language) => language.startsWith("pa") && (language.includes("arab") || language.includes("-pk")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
locale: "az",
|
|
||||||
match: (language) => language.startsWith("az") && !language.includes("arab") && !language.includes("cyrl"),
|
|
||||||
},
|
|
||||||
{ locale: "fi", match: (language) => language.startsWith("fi") },
|
|
||||||
{ locale: "sv", match: (language) => language.startsWith("sv") },
|
|
||||||
]
|
|
||||||
|
|
||||||
function detectLocale(): Locale {
|
function detectLocale(): Locale {
|
||||||
if (typeof navigator !== "object") return "en"
|
if (typeof navigator !== "object") return "en"
|
||||||
|
return detectDesktopNativeLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
|
||||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
|
||||||
for (const language of languages) {
|
|
||||||
if (!language) continue
|
|
||||||
const normalized = language.toLowerCase()
|
|
||||||
const match = localeMatchers.find((entry) => entry.match(normalized))
|
|
||||||
if (match) return match.locale
|
|
||||||
}
|
|
||||||
|
|
||||||
return "en"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeLocale(value: string): Locale {
|
export function normalizeLocale(value: string): Locale {
|
||||||
@@ -198,7 +151,17 @@ function readStoredLocale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const warm = readStoredLocale() ?? detectLocale()
|
const warm = readStoredLocale() ?? detectLocale()
|
||||||
if (warm !== "en") void loadDict(warm)
|
const initialLocale =
|
||||||
|
warm === "en"
|
||||||
|
? Promise.resolve(warm)
|
||||||
|
: loadDict(warm).then(
|
||||||
|
() => warm,
|
||||||
|
() => "en" as const,
|
||||||
|
)
|
||||||
|
|
||||||
|
export function loadInitialLocale() {
|
||||||
|
return initialLocale
|
||||||
|
}
|
||||||
|
|
||||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||||
name: "Language",
|
name: "Language",
|
||||||
@@ -244,7 +207,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof document !== "object") return
|
if (typeof document !== "object") return
|
||||||
const value = locale()
|
const value = locale()
|
||||||
document.documentElement.lang = value
|
document.documentElement.lang = intl()
|
||||||
document.documentElement.dir = direction()
|
document.documentElement.dir = direction()
|
||||||
document.cookie = cookie(value)
|
document.cookie = cookie(value)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ describe("server session", () => {
|
|||||||
|
|
||||||
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
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.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 () => {
|
test("extends a current page to include the user for split assistant turns", async () => {
|
||||||
@@ -1497,7 +1498,7 @@ describe("server session", () => {
|
|||||||
|
|
||||||
await store.sync("child", { force: true })
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
expect(store.data.message.child).toEqual([boundary, older])
|
expect(store.data.message.child).toEqual([older, boundary])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves a part update for a message being loaded from history", async () => {
|
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 { sessionNotFoundError } from "@/utils/server-errors"
|
||||||
import { rootSession } from "@/utils/session-route"
|
import { rootSession } from "@/utils/session-route"
|
||||||
import { normalizeSessionInfo } from "@/utils/session"
|
import { normalizeSessionInfo } from "@/utils/session"
|
||||||
import { normalizeSessionMessages } from "@/utils/session-message"
|
import { compareMessages, messageKey, normalizeSessionMessages } from "@/utils/session-message"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
||||||
import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
|
import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
|
||||||
import type { ServerApi } from "@/utils/server"
|
import type { ServerApi } from "@/utils/server"
|
||||||
@@ -26,7 +26,6 @@ import type { ServerApi } from "@/utils/server"
|
|||||||
type MessageApi = ServerApi["message"]
|
type MessageApi = ServerApi["message"]
|
||||||
|
|
||||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
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 SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
const initialMessagePageSize = 20
|
const initialMessagePageSize = 20
|
||||||
const historyMessagePageSize = 200
|
const historyMessagePageSize = 200
|
||||||
@@ -64,7 +63,7 @@ type MessagePage = {
|
|||||||
function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
|
function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
|
||||||
return items
|
return items
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => cmp(a.info.id, b.info.id))
|
.sort((a, b) => compareMessages(a.info, b.info))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.info.role === "user") {
|
if (item.info.role === "user") {
|
||||||
return {
|
return {
|
||||||
@@ -111,17 +110,16 @@ function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
|||||||
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
||||||
const observed: { messageID: string; parts: Part[] }[] = []
|
const observed: { messageID: string; parts: Part[] }[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||||
if (!result.found) session.splice(result.index, 0, item.message)
|
const found = result.found
|
||||||
|
if (!found) session.splice(result.index, 0, item.message)
|
||||||
const current = part.get(item.message.id)
|
const current = part.get(item.message.id)
|
||||||
const confirmed = result.found
|
const confirmed = found ? item.parts.filter((part) => current?.some((value) => value.id === part.id)) : []
|
||||||
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
|
if (found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||||
: []
|
|
||||||
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
|
|
||||||
part.set(
|
part.set(
|
||||||
item.message.id,
|
item.message.id,
|
||||||
merge(
|
merge(
|
||||||
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||||
item.parts.filter((part) => !confirmed.includes(part)),
|
item.parts.filter((part) => !confirmed.includes(part)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -158,6 +156,7 @@ function reconcileFetched<T extends { id: string }>(
|
|||||||
retained?: ReadonlySet<string>
|
retained?: ReadonlySet<string>
|
||||||
removed?: ReadonlySet<string>
|
removed?: ReadonlySet<string>
|
||||||
preserveUnfetched?: boolean | ((item: T) => boolean)
|
preserveUnfetched?: boolean | ((item: T) => boolean)
|
||||||
|
compare?: (a: T, b: T) => number
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const result = new Map(fetched.map((item) => [item.id, item]))
|
const result = new Map(fetched.map((item) => [item.id, item]))
|
||||||
@@ -180,7 +179,8 @@ function reconcileFetched<T extends { id: string }>(
|
|||||||
if (!item) result.delete(id)
|
if (!item) result.delete(id)
|
||||||
}
|
}
|
||||||
for (const id of options.removed ?? emptyIDs) result.delete(id)
|
for (const id of options.removed ?? emptyIDs) result.delete(id)
|
||||||
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
|
const items = [...result.values()]
|
||||||
|
return options.compare ? items.sort(options.compare) : items
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
|
type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
|
||||||
@@ -413,8 +413,7 @@ export function createServerSession(
|
|||||||
if (!load) return
|
if (!load) return
|
||||||
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
|
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
|
||||||
const messages = data.message[sessionID]
|
const messages = data.message[sessionID]
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
const parts = load.touchedParts.get(messageID)
|
const parts = load.touchedParts.get(messageID)
|
||||||
if (parts) {
|
if (parts) {
|
||||||
parts.add(partID)
|
parts.add(partID)
|
||||||
@@ -437,16 +436,14 @@ export function createServerSession(
|
|||||||
load.touchedParts.set(messageID, new Set(parts))
|
load.touchedParts.set(messageID, new Set(parts))
|
||||||
load.carriedDeltaParts.set(messageID, new Set(parts))
|
load.carriedDeltaParts.set(messageID, new Set(parts))
|
||||||
const messages = data.message[sessionID]
|
const messages = data.message[sessionID]
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
}
|
}
|
||||||
for (const [messageID, parts] of load.removedParts) {
|
for (const [messageID, parts] of load.removedParts) {
|
||||||
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
||||||
parts.forEach((partID) => touched.add(partID))
|
parts.forEach((partID) => touched.add(partID))
|
||||||
load.touchedParts.set(messageID, touched)
|
load.touchedParts.set(messageID, touched)
|
||||||
const messages = data.message[sessionID]
|
const messages = data.message[sessionID]
|
||||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||||
load.retainedMessages.add(messageID)
|
|
||||||
}
|
}
|
||||||
for (const [messageID, parts] of load.optimisticParts) {
|
for (const [messageID, parts] of load.optimisticParts) {
|
||||||
load.removedMessages.delete(messageID)
|
load.removedMessages.delete(messageID)
|
||||||
@@ -555,7 +552,7 @@ export function createServerSession(
|
|||||||
const source = pages.flatMap((page) => page.data).toReversed()
|
const source = pages.flatMap((page) => page.data).toReversed()
|
||||||
const normalized = normalizeSessionMessages(sessionID, source)
|
const normalized = normalizeSessionMessages(sessionID, source)
|
||||||
return {
|
return {
|
||||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
session: normalized.messages.sort(compareMessages),
|
||||||
part: [...normalized.parts.entries()]
|
part: [...normalized.parts.entries()]
|
||||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||||
.sort((a, b) => cmp(a.id, b.id)),
|
.sort((a, b) => cmp(a.id, b.id)),
|
||||||
@@ -572,7 +569,7 @@ export function createServerSession(
|
|||||||
})
|
})
|
||||||
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
||||||
return {
|
return {
|
||||||
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
session: items.map((item) => cleanMessage(item.info)).sort(compareMessages),
|
||||||
part: items.map((item) => ({
|
part: items.map((item) => ({
|
||||||
id: item.info.id,
|
id: item.info.id,
|
||||||
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
@@ -696,7 +693,7 @@ export function createServerSession(
|
|||||||
const normalized = normalizeSessionMessages(sessionID, source)
|
const normalized = normalizeSessionMessages(sessionID, source)
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
session: normalized.messages.sort(compareMessages),
|
||||||
part: [...normalized.parts.entries()]
|
part: [...normalized.parts.entries()]
|
||||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||||
.sort((a, b) => cmp(a.id, b.id)),
|
.sort((a, b) => cmp(a.id, b.id)),
|
||||||
@@ -713,6 +710,7 @@ export function createServerSession(
|
|||||||
retained: load?.retainedMessages,
|
retained: load?.retainedMessages,
|
||||||
removed: load?.removedMessages,
|
removed: load?.removedMessages,
|
||||||
preserveUnfetched,
|
preserveUnfetched,
|
||||||
|
compare: compareMessages,
|
||||||
})
|
})
|
||||||
batch(() => {
|
batch(() => {
|
||||||
if (source) setData("session_message", sessionID, reconcile(source))
|
if (source) setData("session_message", sessionID, reconcile(source))
|
||||||
@@ -754,7 +752,7 @@ export function createServerSession(
|
|||||||
try {
|
try {
|
||||||
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||||
const first = page.session.reduce<Message | undefined>(
|
const first = page.session.reduce<Message | undefined>(
|
||||||
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
(oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
if (generations.get(sessionID) !== active) return
|
if (generations.get(sessionID) !== active) return
|
||||||
@@ -804,14 +802,15 @@ export function createServerSession(
|
|||||||
session: merge(
|
session: merge(
|
||||||
page.session,
|
page.session,
|
||||||
parents.map((parent) => parent.message),
|
parents.map((parent) => parent.message),
|
||||||
),
|
).sort(compareMessages),
|
||||||
part: merge(
|
part: merge(
|
||||||
page.part,
|
page.part,
|
||||||
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
|
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
const preserveUnfetched =
|
const preserveUnfetched =
|
||||||
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
mode === "prepend" ||
|
||||||
|
(!result.complete && (!first || ((message: Message) => compareMessages(message, first) < 0)))
|
||||||
applyMessagePage(
|
applyMessagePage(
|
||||||
sessionID,
|
sessionID,
|
||||||
result,
|
result,
|
||||||
@@ -928,7 +927,7 @@ export function createServerSession(
|
|||||||
.message({ sessionID, messageID })
|
.message({ sessionID, messageID })
|
||||||
.then((message) => {
|
.then((message) => {
|
||||||
const current = data.session_message[sessionID] ?? []
|
const current = data.session_message[sessionID] ?? []
|
||||||
const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id))
|
const messages = [...current.filter((item) => item.id !== message.id), message].sort(compareMessages)
|
||||||
projectV2({ sessionID, messages, touched: [message.id] })
|
projectV2({ sessionID, messages, touched: [message.id] })
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -1051,7 +1050,7 @@ export function createServerSession(
|
|||||||
setData("message", info.sessionID, [info])
|
setData("message", info.sessionID, [info])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = Binary.search(messages, info.id, (message) => message.id)
|
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||||
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
||||||
if (!result.found)
|
if (!result.found)
|
||||||
setData("message", info.sessionID, (value = []) => {
|
setData("message", info.sessionID, (value = []) => {
|
||||||
@@ -1084,8 +1083,8 @@ export function createServerSession(
|
|||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const messages = draft.message[props.sessionID]
|
const messages = draft.message[props.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (index >= 0) messages.splice(index, 1)
|
||||||
}
|
}
|
||||||
deleteMessageParts(draft, props.messageID)
|
deleteMessageParts(draft, props.messageID)
|
||||||
}),
|
}),
|
||||||
@@ -1097,7 +1096,7 @@ export function createServerSession(
|
|||||||
if (SKIP_PARTS.has(part.type)) return
|
if (SKIP_PARTS.has(part.type)) return
|
||||||
const messages = data.message[part.sessionID]
|
const messages = data.message[part.sessionID]
|
||||||
const load = messageLoads.get(part.sessionID)
|
const load = messageLoads.get(part.sessionID)
|
||||||
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
|
const missing = !messages?.some((message) => message.id === part.messageID)
|
||||||
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
||||||
if (
|
if (
|
||||||
missing &&
|
missing &&
|
||||||
@@ -1341,7 +1340,7 @@ export function createServerSession(
|
|||||||
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
|
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
|
||||||
if (!items)
|
if (!items)
|
||||||
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
|
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
|
||||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]).sort(compareMessages))
|
||||||
setData(
|
setData(
|
||||||
"part_text_accum_delta",
|
"part_text_accum_delta",
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "
|
|||||||
|
|
||||||
type Text = Extract<Part, { type: "text" }>
|
type Text = Extract<Part, { type: "text" }>
|
||||||
|
|
||||||
const userMessage = (id: string, sessionID: string): Message => ({
|
const userMessage = (id: string, sessionID: string, created = 1): Message => ({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: { created: 1 },
|
time: { created },
|
||||||
agent: "assistant",
|
agent: "assistant",
|
||||||
model: { providerID: "openai", modelID: "gpt" },
|
model: { providerID: "openai", modelID: "gpt" },
|
||||||
})
|
})
|
||||||
@@ -22,21 +22,21 @@ const textPart = (id: string, sessionID: string, messageID: string): Text => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("sync optimistic reducers", () => {
|
describe("sync optimistic reducers", () => {
|
||||||
test("applyOptimisticAdd inserts message in sorted order and stores parts", () => {
|
test("applyOptimisticAdd inserts by creation time", () => {
|
||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const draft = {
|
const draft = {
|
||||||
message: { [sessionID]: [userMessage("msg_2", sessionID)] },
|
message: { [sessionID]: [userMessage("msg_z", sessionID, 1)] },
|
||||||
part: {} as Record<string, Part[] | undefined>,
|
part: {} as Record<string, Part[] | undefined>,
|
||||||
}
|
}
|
||||||
|
|
||||||
applyOptimisticAdd(draft, {
|
applyOptimisticAdd(draft, {
|
||||||
sessionID,
|
sessionID,
|
||||||
message: userMessage("msg_1", sessionID),
|
message: userMessage("msg_a", sessionID, 2),
|
||||||
parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")],
|
parts: [textPart("prt_2", sessionID, "msg_a"), textPart("prt_1", sessionID, "msg_a")],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||||
expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
expect(draft.part.msg_a?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applyOptimisticRemove removes message and part entries", () => {
|
test("applyOptimisticRemove removes message and part entries", () => {
|
||||||
@@ -60,19 +60,33 @@ describe("sync optimistic reducers", () => {
|
|||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const page = mergeOptimisticPage(
|
const page = mergeOptimisticPage(
|
||||||
{
|
{
|
||||||
session: [userMessage("msg_1", sessionID)],
|
session: [userMessage("msg_z", sessionID, 1)],
|
||||||
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
part: [{ id: "msg_z", part: [textPart("prt_1", sessionID, "msg_z")] }],
|
||||||
complete: true,
|
complete: true,
|
||||||
},
|
},
|
||||||
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
[{ message: userMessage("msg_a", sessionID, 2), parts: [textPart("prt_2", sessionID, "msg_a")] }],
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
expect(page.session.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||||
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
expect(page.part.find((x) => x.id === "msg_a")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||||
expect(page.confirmed).toEqual([])
|
expect(page.confirmed).toEqual([])
|
||||||
expect(page.complete).toBe(true)
|
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", () => {
|
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
|
||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const page = mergeOptimisticPage(
|
const page = mergeOptimisticPage(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createMemo } from "solid-js"
|
|||||||
import { useServerSync } from "./server-sync"
|
import { useServerSync } from "./server-sync"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
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"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
|||||||
const confirmed: string[] = []
|
const confirmed: string[] = []
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||||
const found = result.found
|
const found = result.found
|
||||||
if (!found) session.splice(result.index, 0, item.message)
|
if (!found) session.splice(result.index, 0, item.message)
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
|||||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||||
const messages = draft.message[input.sessionID]
|
const messages = draft.message[input.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
const result = Binary.search(messages, messageKey(input.message), messageKey)
|
||||||
messages.splice(result.index, 0, input.message)
|
messages.splice(result.index, 0, input.message)
|
||||||
} else {
|
} else {
|
||||||
draft.message[input.sessionID] = [input.message]
|
draft.message[input.sessionID] = [input.message]
|
||||||
@@ -103,8 +104,8 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI
|
|||||||
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
||||||
const messages = draft.message[input.sessionID]
|
const messages = draft.message[input.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
const index = messages.findIndex((message) => message.id === input.messageID)
|
||||||
if (result.found) messages.splice(result.index, 1)
|
if (index >= 0) messages.splice(index, 1)
|
||||||
}
|
}
|
||||||
delete draft.part[input.messageID]
|
delete draft.part[input.messageID]
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-25
@@ -3,6 +3,7 @@
|
|||||||
import * as Sentry from "@sentry/solid"
|
import * as Sentry from "@sentry/solid"
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { AppBaseProviders, AppInterface } from "@/app"
|
import { AppBaseProviders, AppInterface } from "@/app"
|
||||||
|
import { loadInitialLocale } from "@/context/language"
|
||||||
import { type Platform, PlatformProvider } from "@/context/platform"
|
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||||
import { createBrowserDraftStore } from "@/utils/draft-store"
|
import { createBrowserDraftStore } from "@/utils/draft-store"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
@@ -149,29 +150,31 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (root instanceof HTMLElement) {
|
if (root instanceof HTMLElement) {
|
||||||
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
void loadInitialLocale().then((locale) => {
|
||||||
clearAuthToken()
|
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||||
const server: ServerConnection.Http = {
|
clearAuthToken()
|
||||||
type: "http",
|
const server: ServerConnection.Http = {
|
||||||
authToken: !!auth,
|
type: "http",
|
||||||
http: {
|
authToken: !!auth,
|
||||||
url: getCurrentUrl(),
|
http: {
|
||||||
...auth,
|
url: getCurrentUrl(),
|
||||||
},
|
...auth,
|
||||||
}
|
},
|
||||||
render(
|
}
|
||||||
() => (
|
render(
|
||||||
<PlatformProvider value={platform}>
|
() => (
|
||||||
<AppBaseProviders>
|
<PlatformProvider value={platform}>
|
||||||
<AppInterface
|
<AppBaseProviders locale={locale}>
|
||||||
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
<AppInterface
|
||||||
canonicalLocalServer={ServerConnection.key(server)}
|
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||||
servers={[server]}
|
canonicalLocalServer={ServerConnection.key(server)}
|
||||||
disableHealthCheck
|
servers={[server]}
|
||||||
/>
|
disableHealthCheck
|
||||||
</AppBaseProviders>
|
/>
|
||||||
</PlatformProvider>
|
</AppBaseProviders>
|
||||||
),
|
</PlatformProvider>
|
||||||
root,
|
),
|
||||||
)
|
root,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ import {
|
|||||||
DESKTOP_NATIVE_KEYS,
|
DESKTOP_NATIVE_KEYS,
|
||||||
DESKTOP_NATIVE_LABELS,
|
DESKTOP_NATIVE_LABELS,
|
||||||
DESKTOP_NATIVE_LOCALES,
|
DESKTOP_NATIVE_LOCALES,
|
||||||
|
DESKTOP_NATIVE_LOCALE_TAGS,
|
||||||
|
detectDesktopNativeLocale,
|
||||||
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
|
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
|
||||||
formatDesktopNativeMessage,
|
formatDesktopNativeMessage,
|
||||||
parseDesktopNativeBundle,
|
parseDesktopNativeBundle,
|
||||||
@@ -41,6 +43,40 @@ describe("desktop native translations", () => {
|
|||||||
"Azərbaycanca",
|
"Azərbaycanca",
|
||||||
"Suomi",
|
"Suomi",
|
||||||
"Svenska",
|
"Svenska",
|
||||||
|
"አማርኛ",
|
||||||
|
"Български",
|
||||||
|
"বাংলা",
|
||||||
|
"Català",
|
||||||
|
"Čeština",
|
||||||
|
"ދިވެހި",
|
||||||
|
"རྫོང་ཁ",
|
||||||
|
"Ελληνικά",
|
||||||
|
"Eesti",
|
||||||
|
"فارسی",
|
||||||
|
"Føroyskt",
|
||||||
|
"Hrvatski",
|
||||||
|
"Magyar",
|
||||||
|
"Հայերեն",
|
||||||
|
"Íslenska",
|
||||||
|
"ქართული",
|
||||||
|
"ខ្មែរ",
|
||||||
|
"ລາວ",
|
||||||
|
"Lietuvių",
|
||||||
|
"Latviešu",
|
||||||
|
"Македонски",
|
||||||
|
"Монгол",
|
||||||
|
"Bahasa Melayu",
|
||||||
|
"မြန်မာ",
|
||||||
|
"नेपाली",
|
||||||
|
"Română",
|
||||||
|
"සිංහල",
|
||||||
|
"Slovenčina",
|
||||||
|
"Slovenščina",
|
||||||
|
"Shqip",
|
||||||
|
"Српски",
|
||||||
|
"Тоҷикӣ",
|
||||||
|
"Türkmençe",
|
||||||
|
"Oʻzbekcha",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,3 +110,42 @@ describe("desktop native translations", () => {
|
|||||||
expect(formatDesktopNativeMessage("{{known}} {{unknown}}", { known: "yes" })).toBe("yes {{unknown}}")
|
expect(formatDesktopNativeMessage("{{known}} {{unknown}}", { known: "yes" })).toBe("yes {{unknown}}")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("desktop native locale detection", () => {
|
||||||
|
test("follows preference order and skips invalid or unsupported tags", () => {
|
||||||
|
expect(detectDesktopNativeLocale(["not_a_locale", "fr-FR"])).toBe("fr")
|
||||||
|
expect(detectDesktopNativeLocale(["eo", "de-DE"])).toBe("de")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses Unicode likely subtags for script-sensitive bundles", () => {
|
||||||
|
expect(detectDesktopNativeLocale(["zh-TW"])).toBe("zht")
|
||||||
|
expect(detectDesktopNativeLocale(["zh-SG"])).toBe("zh")
|
||||||
|
expect(detectDesktopNativeLocale(["pa-PK"])).toBe("pa")
|
||||||
|
expect(detectDesktopNativeLocale(["pa-IN", "fr"])).toBe("fr")
|
||||||
|
expect(detectDesktopNativeLocale(["az-Cyrl", "de"])).toBe("de")
|
||||||
|
expect(detectDesktopNativeLocale(["sr-Cyrl"])).toBe("sr")
|
||||||
|
expect(detectDesktopNativeLocale(["sr-Latn", "en"])).toBe("en")
|
||||||
|
expect(detectDesktopNativeLocale(["uz-Latn"])).toBe("uz")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("recognizes Norwegian language tags", () => {
|
||||||
|
expect(detectDesktopNativeLocale(["no"])).toBe("no")
|
||||||
|
expect(detectDesktopNativeLocale(["nb-NO"])).toBe("no")
|
||||||
|
expect(detectDesktopNativeLocale(["nn-NO"])).toBe("no")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("desktop native ICU data", () => {
|
||||||
|
test("accepts every locale in standard Intl formatters", () => {
|
||||||
|
for (const locale of DESKTOP_NATIVE_LOCALES) {
|
||||||
|
const tag = DESKTOP_NATIVE_LOCALE_TAGS[locale]
|
||||||
|
expect(() => new Intl.Locale(tag), `${locale} locale`).not.toThrow()
|
||||||
|
expect(() => new Intl.NumberFormat(tag), `${locale} number`).not.toThrow()
|
||||||
|
expect(() => new Intl.DateTimeFormat(tag), `${locale} date`).not.toThrow()
|
||||||
|
expect(() => new Intl.PluralRules(tag), `${locale} plural`).not.toThrow()
|
||||||
|
expect(() => new Intl.ListFormat(tag), `${locale} list`).not.toThrow()
|
||||||
|
expect(() => new Intl.DisplayNames(tag, { type: "language" }), `${locale} names`).not.toThrow()
|
||||||
|
expect(() => new Intl.Segmenter(tag), `${locale} segmenter`).not.toThrow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -27,6 +27,40 @@ export const DESKTOP_NATIVE_LOCALES = [
|
|||||||
"az",
|
"az",
|
||||||
"fi",
|
"fi",
|
||||||
"sv",
|
"sv",
|
||||||
|
"am",
|
||||||
|
"bg",
|
||||||
|
"bn",
|
||||||
|
"ca",
|
||||||
|
"cs",
|
||||||
|
"dv",
|
||||||
|
"dz",
|
||||||
|
"el",
|
||||||
|
"et",
|
||||||
|
"fa",
|
||||||
|
"fo",
|
||||||
|
"hr",
|
||||||
|
"hu",
|
||||||
|
"hy",
|
||||||
|
"is",
|
||||||
|
"ka",
|
||||||
|
"km",
|
||||||
|
"lo",
|
||||||
|
"lt",
|
||||||
|
"lv",
|
||||||
|
"mk",
|
||||||
|
"mn",
|
||||||
|
"ms",
|
||||||
|
"my",
|
||||||
|
"ne",
|
||||||
|
"ro",
|
||||||
|
"si",
|
||||||
|
"sk",
|
||||||
|
"sl",
|
||||||
|
"sq",
|
||||||
|
"sr",
|
||||||
|
"tg",
|
||||||
|
"tk",
|
||||||
|
"uz",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number]
|
export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number]
|
||||||
@@ -60,6 +94,131 @@ export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
|
|||||||
az: "Azərbaycanca",
|
az: "Azərbaycanca",
|
||||||
fi: "Suomi",
|
fi: "Suomi",
|
||||||
sv: "Svenska",
|
sv: "Svenska",
|
||||||
|
am: "አማርኛ",
|
||||||
|
bg: "Български",
|
||||||
|
bn: "বাংলা",
|
||||||
|
ca: "Català",
|
||||||
|
cs: "Čeština",
|
||||||
|
dv: "ދިވެހި",
|
||||||
|
dz: "རྫོང་ཁ",
|
||||||
|
el: "Ελληνικά",
|
||||||
|
et: "Eesti",
|
||||||
|
fa: "فارسی",
|
||||||
|
fo: "Føroyskt",
|
||||||
|
hr: "Hrvatski",
|
||||||
|
hu: "Magyar",
|
||||||
|
hy: "Հայերեն",
|
||||||
|
is: "Íslenska",
|
||||||
|
ka: "ქართული",
|
||||||
|
km: "ខ្មែរ",
|
||||||
|
lo: "ລາວ",
|
||||||
|
lt: "Lietuvių",
|
||||||
|
lv: "Latviešu",
|
||||||
|
mk: "Македонски",
|
||||||
|
mn: "Монгол",
|
||||||
|
ms: "Bahasa Melayu",
|
||||||
|
my: "မြန်မာ",
|
||||||
|
ne: "नेपाली",
|
||||||
|
ro: "Română",
|
||||||
|
si: "සිංහල",
|
||||||
|
sk: "Slovenčina",
|
||||||
|
sl: "Slovenščina",
|
||||||
|
sq: "Shqip",
|
||||||
|
sr: "Српски",
|
||||||
|
tg: "Тоҷикӣ",
|
||||||
|
tk: "Türkmençe",
|
||||||
|
uz: "Oʻzbekcha",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DESKTOP_NATIVE_LOCALE_TAGS: Record<DesktopNativeLocale, string> = {
|
||||||
|
en: "en",
|
||||||
|
zh: "zh-Hans",
|
||||||
|
zht: "zh-Hant",
|
||||||
|
ko: "ko",
|
||||||
|
de: "de",
|
||||||
|
es: "es",
|
||||||
|
fr: "fr",
|
||||||
|
da: "da",
|
||||||
|
ja: "ja",
|
||||||
|
pl: "pl",
|
||||||
|
ru: "ru",
|
||||||
|
uk: "uk",
|
||||||
|
bs: "bs",
|
||||||
|
ar: "ar",
|
||||||
|
no: "nb-NO",
|
||||||
|
br: "pt-BR",
|
||||||
|
th: "th",
|
||||||
|
tr: "tr",
|
||||||
|
hi: "hi-IN",
|
||||||
|
nl: "nl-NL",
|
||||||
|
id: "id-ID",
|
||||||
|
vi: "vi-VN",
|
||||||
|
it: "it-IT",
|
||||||
|
ur: "ur-PK",
|
||||||
|
pa: "pa-Arab-PK",
|
||||||
|
az: "az-Latn-AZ",
|
||||||
|
fi: "fi-FI",
|
||||||
|
sv: "sv-SE",
|
||||||
|
am: "am-ET",
|
||||||
|
bg: "bg-BG",
|
||||||
|
bn: "bn-BD",
|
||||||
|
ca: "ca-AD",
|
||||||
|
cs: "cs-CZ",
|
||||||
|
dv: "dv-MV",
|
||||||
|
dz: "dz-BT",
|
||||||
|
el: "el-GR",
|
||||||
|
et: "et-EE",
|
||||||
|
fa: "fa-IR",
|
||||||
|
fo: "fo-FO",
|
||||||
|
hr: "hr-HR",
|
||||||
|
hu: "hu-HU",
|
||||||
|
hy: "hy-AM",
|
||||||
|
is: "is-IS",
|
||||||
|
ka: "ka-GE",
|
||||||
|
km: "km-KH",
|
||||||
|
lo: "lo-LA",
|
||||||
|
lt: "lt-LT",
|
||||||
|
lv: "lv-LV",
|
||||||
|
mk: "mk-MK",
|
||||||
|
mn: "mn-MN",
|
||||||
|
ms: "ms-MY",
|
||||||
|
my: "my-MM",
|
||||||
|
ne: "ne-NP",
|
||||||
|
ro: "ro-RO",
|
||||||
|
si: "si-LK",
|
||||||
|
sk: "sk-SK",
|
||||||
|
sl: "sl-SI",
|
||||||
|
sq: "sq-AL",
|
||||||
|
sr: "sr-Cyrl-RS",
|
||||||
|
tg: "tg-Cyrl-TJ",
|
||||||
|
tk: "tk-Latn-TM",
|
||||||
|
uz: "uz-Latn-UZ",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectDesktopNativeLocale(languages: readonly string[]): DesktopNativeLocale {
|
||||||
|
for (const language of languages) {
|
||||||
|
const source = locale(language)
|
||||||
|
if (!source) continue
|
||||||
|
if (["no", "nb", "nn"].includes(source.language)) return "no"
|
||||||
|
const match = DESKTOP_NATIVE_LOCALES.find((candidate) => {
|
||||||
|
const target = locale(DESKTOP_NATIVE_LOCALE_TAGS[candidate])
|
||||||
|
return target?.language === source.language && target.script === source.script
|
||||||
|
})
|
||||||
|
if (match) return match
|
||||||
|
}
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function desktopNativePluralCategories(locale: DesktopNativeLocale) {
|
||||||
|
return new Intl.PluralRules(DESKTOP_NATIVE_LOCALE_TAGS[locale]).resolvedOptions().pluralCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
function locale(value: string) {
|
||||||
|
try {
|
||||||
|
return new Intl.Locale(value).maximize()
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DESKTOP_NATIVE_ENGLISH = {
|
export const DESKTOP_NATIVE_ENGLISH = {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { desktopNativePluralCategories } from "./desktop-native"
|
||||||
|
|
||||||
const appLocales = [
|
const appLocales = [
|
||||||
"ar",
|
"ar",
|
||||||
@@ -28,19 +29,51 @@ const appLocales = [
|
|||||||
"az",
|
"az",
|
||||||
"fi",
|
"fi",
|
||||||
"sv",
|
"sv",
|
||||||
|
"am",
|
||||||
|
"bg",
|
||||||
|
"bn",
|
||||||
|
"ca",
|
||||||
|
"cs",
|
||||||
|
"dv",
|
||||||
|
"dz",
|
||||||
|
"el",
|
||||||
|
"et",
|
||||||
|
"fa",
|
||||||
|
"fo",
|
||||||
|
"hr",
|
||||||
|
"hu",
|
||||||
|
"hy",
|
||||||
|
"is",
|
||||||
|
"ka",
|
||||||
|
"km",
|
||||||
|
"lo",
|
||||||
|
"lt",
|
||||||
|
"lv",
|
||||||
|
"mk",
|
||||||
|
"mn",
|
||||||
|
"ms",
|
||||||
|
"my",
|
||||||
|
"ne",
|
||||||
|
"ro",
|
||||||
|
"si",
|
||||||
|
"sk",
|
||||||
|
"sl",
|
||||||
|
"sq",
|
||||||
|
"sr",
|
||||||
|
"tg",
|
||||||
|
"tk",
|
||||||
|
"uz",
|
||||||
] as const
|
] as const
|
||||||
const desktopLocales = appLocales
|
const desktopLocales = appLocales
|
||||||
const pluralCategories: Partial<Record<(typeof appLocales)[number], readonly string[]>> = {
|
const pluralCategories = new Map(
|
||||||
ar: ["zero", "two", "few", "many"],
|
appLocales.map(
|
||||||
br: ["many"],
|
(locale) =>
|
||||||
bs: ["few"],
|
[
|
||||||
es: ["many"],
|
locale,
|
||||||
fr: ["many"],
|
desktopNativePluralCategories(locale).filter((category) => category !== "one" && category !== "other"),
|
||||||
it: ["many"],
|
] as const,
|
||||||
pl: ["few", "many"],
|
),
|
||||||
ru: ["few", "many"],
|
)
|
||||||
uk: ["few", "many"],
|
|
||||||
}
|
|
||||||
|
|
||||||
const domains = [
|
const domains = [
|
||||||
{
|
{
|
||||||
@@ -74,7 +107,7 @@ describe("i18n parity", () => {
|
|||||||
.filter((key) => !Object.hasOwn(source, key))
|
.filter((key) => !Object.hasOwn(source, key))
|
||||||
.sort()
|
.sort()
|
||||||
const expected = pluralFamilies(source)
|
const expected = pluralFamilies(source)
|
||||||
.flatMap((key) => (pluralCategories[locale] ?? []).map((category) => `${key}.${category}`))
|
.flatMap((key) => (pluralCategories.get(locale) ?? []).map((category) => `${key}.${category}`))
|
||||||
.sort()
|
.sort()
|
||||||
expect({ domain: domain.name, locale, missing, extra }).toEqual({
|
expect({ domain: domain.name, locale, missing, extra }).toEqual({
|
||||||
domain: domain.name,
|
domain: domain.name,
|
||||||
@@ -95,7 +128,7 @@ describe("i18n parity", () => {
|
|||||||
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
|
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
|
||||||
)
|
)
|
||||||
const pluralMismatched = pluralFamilies(source).flatMap((key) =>
|
const pluralMismatched = pluralFamilies(source).flatMap((key) =>
|
||||||
(pluralCategories[locale] ?? [])
|
(pluralCategories.get(locale) ?? [])
|
||||||
.map((category) => `${key}.${category}`)
|
.map((category) => `${key}.${category}`)
|
||||||
.filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()),
|
.filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()),
|
||||||
)
|
)
|
||||||
@@ -144,12 +177,12 @@ describe("i18n plural parity", () => {
|
|||||||
for (const locale of domain.locales) {
|
for (const locale of domain.locales) {
|
||||||
const target = await dictionary(domain.target(locale))
|
const target = await dictionary(domain.target(locale))
|
||||||
const missing = families.flatMap((key) =>
|
const missing = families.flatMap((key) =>
|
||||||
(pluralCategories[locale] ?? [])
|
(pluralCategories.get(locale) ?? [])
|
||||||
.map((category) => `${key}.${category}`)
|
.map((category) => `${key}.${category}`)
|
||||||
.filter((variant) => !Object.hasOwn(target, variant)),
|
.filter((variant) => !Object.hasOwn(target, variant)),
|
||||||
)
|
)
|
||||||
const mismatched = families.flatMap((key) =>
|
const mismatched = families.flatMap((key) =>
|
||||||
(pluralCategories[locale] ?? [])
|
(pluralCategories.get(locale) ?? [])
|
||||||
.map((category) => `${key}.${category}`)
|
.map((category) => `${key}.${category}`)
|
||||||
.filter(
|
.filter(
|
||||||
(variant) =>
|
(variant) =>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -583,7 +583,7 @@ export const dict = {
|
|||||||
"dialog.usageExceeded.dontShowAgain": "不再显示",
|
"dialog.usageExceeded.dontShowAgain": "不再显示",
|
||||||
|
|
||||||
"context.breakdown.title": "上下文细分",
|
"context.breakdown.title": "上下文细分",
|
||||||
"context.breakdown.note": "输入令牌的大致细分。“其他”包含工具定义和开销。",
|
"context.breakdown.note": "输入词元的大致细分。“其他”包含工具定义和开销。",
|
||||||
"context.breakdown.system": "系统",
|
"context.breakdown.system": "系统",
|
||||||
"context.breakdown.user": "用户",
|
"context.breakdown.user": "用户",
|
||||||
"context.breakdown.assistant": "助手",
|
"context.breakdown.assistant": "助手",
|
||||||
@@ -598,18 +598,18 @@ export const dict = {
|
|||||||
"context.stats.provider": "提供商",
|
"context.stats.provider": "提供商",
|
||||||
"context.stats.model": "模型",
|
"context.stats.model": "模型",
|
||||||
"context.stats.limit": "上下文限制",
|
"context.stats.limit": "上下文限制",
|
||||||
"context.stats.totalTokens": "令牌总数",
|
"context.stats.totalTokens": "词元总数",
|
||||||
"context.stats.usage": "使用率",
|
"context.stats.usage": "使用率",
|
||||||
"context.stats.inputTokens": "输入令牌",
|
"context.stats.inputTokens": "输入词元",
|
||||||
"context.stats.outputTokens": "输出令牌",
|
"context.stats.outputTokens": "输出词元",
|
||||||
"context.stats.reasoningTokens": "推理令牌",
|
"context.stats.reasoningTokens": "推理词元",
|
||||||
"context.stats.cacheTokens": "缓存令牌(读/写)",
|
"context.stats.cacheTokens": "缓存词元(读/写)",
|
||||||
"context.stats.userMessages": "用户消息",
|
"context.stats.userMessages": "用户消息",
|
||||||
"context.stats.assistantMessages": "助手消息",
|
"context.stats.assistantMessages": "助手消息",
|
||||||
"context.stats.totalCost": "总成本",
|
"context.stats.totalCost": "总成本",
|
||||||
"context.stats.sessionCreated": "创建时间",
|
"context.stats.sessionCreated": "创建时间",
|
||||||
"context.stats.lastActivity": "最后活动",
|
"context.stats.lastActivity": "最后活动",
|
||||||
"context.usage.tokens": "令牌数",
|
"context.usage.tokens": "词元数",
|
||||||
"context.usage.usage": "使用率",
|
"context.usage.usage": "使用率",
|
||||||
"context.usage.cost": "成本",
|
"context.usage.cost": "成本",
|
||||||
"context.usage.clickToView": "点击查看上下文",
|
"context.usage.clickToView": "点击查看上下文",
|
||||||
|
|||||||
@@ -90,7 +90,20 @@ export function createHomeController() {
|
|||||||
const directory = directories[0]
|
const directory = directories[0]
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const ctx = global.ensureServerCtx(conn)
|
const ctx = global.ensureServerCtx(conn)
|
||||||
directories.forEach((item) => ctx.projects.open(item))
|
directories.forEach((item) => {
|
||||||
|
if (ctx.projects.list().some((project) => project.worktree === item)) return
|
||||||
|
const location = { directory: item }
|
||||||
|
void ctx.sdk.api.file
|
||||||
|
.list({ path: ".", location })
|
||||||
|
.then(async (files) => {
|
||||||
|
if (files.data.length > 0) return ctx.sdk.api.project.current({ location })
|
||||||
|
const result = await ctx.sdk.client.project.initGit({ directory: item })
|
||||||
|
return result.data ?? ctx.sdk.api.project.current({ location })
|
||||||
|
})
|
||||||
|
.then((project) => ctx.sync.child(item, { bootstrap: false })[1]("project", project.id))
|
||||||
|
.catch(() => undefined)
|
||||||
|
ctx.projects.open(item)
|
||||||
|
})
|
||||||
ctx.projects.touch(directory)
|
ctx.projects.touch(directory)
|
||||||
setSelection({ server: ServerConnection.key(conn), directory })
|
setSelection({ server: ServerConnection.key(conn), directory })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -475,6 +475,10 @@ function HomeProjectRow(
|
|||||||
ref={sortable.ref}
|
ref={sortable.ref}
|
||||||
class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"
|
class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"
|
||||||
classList={{ "z-10": sortable.isDragSource() }}
|
classList={{ "z-10": sortable.isDragSource() }}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onSetContextMenuOpen(contextMenuID(), true)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<HomeProjectNavButton
|
<HomeProjectNavButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { LocalProject } from "@/context/layout"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/server"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { sessionHasOpenTab, useTabs } from "@/context/tabs"
|
import { sessionHasOpenTab, useTabs } from "@/context/tabs"
|
||||||
import { displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
import { compareSessionTime, displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
@@ -254,7 +254,7 @@ function buildHomeSessionRecords(input: {
|
|||||||
const directories = new Set(input.projectDirectories().map(pathKey))
|
const directories = new Set(input.projectDirectories().map(pathKey))
|
||||||
const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory)))
|
const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory)))
|
||||||
return [...new Map(sessions.map((session) => [session.id, session] as const)).values()]
|
return [...new Map(sessions.map((session) => [session.id, session] as const)).values()]
|
||||||
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
.sort(compareSessionTime)
|
||||||
.flatMap((session) => {
|
.flatMap((session) => {
|
||||||
const directory = pathKey(session.directory)
|
const directory = pathKey(session.directory)
|
||||||
const project =
|
const project =
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
|
|||||||
import {
|
import {
|
||||||
childSessionOnPath,
|
childSessionOnPath,
|
||||||
closeHomeProject,
|
closeHomeProject,
|
||||||
|
compareSessionTime,
|
||||||
displayName,
|
displayName,
|
||||||
effectiveWorkspaceOrder,
|
effectiveWorkspaceOrder,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
homeProjectDirectories,
|
homeProjectDirectories,
|
||||||
homeSessionServerStatus,
|
homeSessionServerStatus,
|
||||||
latestRootSession,
|
latestRootSession,
|
||||||
|
sortedRootSessions,
|
||||||
toggleHomeProjectSelection,
|
toggleHomeProjectSelection,
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
@@ -153,6 +155,30 @@ describe("layout workspace helpers", () => {
|
|||||||
expect(result?.id).toBe("workspace")
|
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", () => {
|
test("detects project permissions with a filter", () => {
|
||||||
const result = hasProjectPermissions(
|
const result = hasProjectPermissions(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,18 +9,10 @@ type SessionStore = {
|
|||||||
path: { directory: string }
|
path: { directory: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortSessions(now: number) {
|
export function compareSessionTime(a: Session, b: Session) {
|
||||||
const oneMinuteAgo = now - 60 * 1000
|
const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)
|
||||||
return (a: Session, b: Session) => {
|
if (updated !== 0) return updated
|
||||||
const aUpdated = a.time.updated ?? a.time.created
|
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||||
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) =>
|
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||||
@@ -29,10 +21,10 @@ const isRootVisibleSession = (session: Session, directory: string) =>
|
|||||||
export const roots = (store: SessionStore) =>
|
export const roots = (store: SessionStore) =>
|
||||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||||
|
|
||||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
export const sortedRootSessions = (store: SessionStore, _now: number) => roots(store).sort(compareSessionTime)
|
||||||
|
|
||||||
export const latestRootSession = (stores: SessionStore[], now: number) =>
|
export const latestRootSession = (stores: SessionStore[], _now: number) =>
|
||||||
stores.flatMap(roots).sort(sortSessions(now))[0]
|
stores.flatMap(roots).sort(compareSessionTime)[0]
|
||||||
|
|
||||||
export function hasProjectPermissions<T>(
|
export function hasProjectPermissions<T>(
|
||||||
request: Record<string, T[] | undefined> | undefined,
|
request: Record<string, T[] | undefined> | undefined,
|
||||||
|
|||||||
@@ -1851,7 +1851,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const session = sdk().api.session
|
const session = sdk().api.session
|
||||||
const target = sync()
|
const target = sync()
|
||||||
const next = userMessages().find((item) => item.id > id)
|
const index = userMessages().findIndex((item) => item.id === id)
|
||||||
|
if (index < 0) return
|
||||||
|
const next = userMessages()[index + 1]
|
||||||
const last = target.session.get(sessionID)?.revert
|
const last = target.session.get(sessionID)?.revert
|
||||||
|
|
||||||
await runPromptRollbackMutation({
|
await runPromptRollbackMutation({
|
||||||
@@ -1891,8 +1893,10 @@ export default function Page() {
|
|||||||
const rolled = createMemo(() => {
|
const rolled = createMemo(() => {
|
||||||
const id = revertMessageID()
|
const id = revertMessageID()
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
|
const index = userMessages().findIndex((item) => item.id === id)
|
||||||
|
if (index < 0) return []
|
||||||
return userMessages()
|
return userMessages()
|
||||||
.filter((item) => item.id >= id)
|
.slice(index)
|
||||||
.map((item) => ({ id: item.id, text: line(item.id) }))
|
.map((item) => ({ id: item.id, text: line(item.id) }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,9 @@ export function MessageTimeline(props: {
|
|||||||
const visible = new Set(props.userMessages.map((message) => message.id))
|
const visible = new Set(props.userMessages.map((message) => message.id))
|
||||||
const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
|
const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
|
||||||
const messages = sync().data.session_message[id] ?? []
|
const messages = sync().data.session_message[id] ?? []
|
||||||
return boundary ? messages.filter((message) => message.id < boundary) : messages
|
if (!boundary) return messages
|
||||||
|
const index = messages.findIndex((message) => message.id === boundary)
|
||||||
|
return index < 0 ? messages : messages.slice(0, index)
|
||||||
})
|
})
|
||||||
const info = createMemo(() => {
|
const info = createMemo(() => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ const assistant = (id: string) => ({ id, role: "assistant" }) as AssistantMessag
|
|||||||
|
|
||||||
describe("timeline model", () => {
|
describe("timeline model", () => {
|
||||||
test("selects users and applies the revert boundary", () => {
|
test("selects users and applies the revert boundary", () => {
|
||||||
const messages: Message[] = [user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_5")]
|
const messages: Message[] = [user("msg_z"), assistant("msg_a"), user("msg_b"), user("msg_c")]
|
||||||
const users = selectUserMessages(messages)
|
const users = selectUserMessages(messages)
|
||||||
|
|
||||||
expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"])
|
expect(users.map((message) => message.id)).toEqual(["msg_z", "msg_b", "msg_c"])
|
||||||
expect(selectVisibleUserMessages(users, "msg_5").map((message) => message.id)).toEqual(["msg_1", "msg_3"])
|
expect(selectVisibleUserMessages(users, "msg_b").map((message) => message.id)).toEqual(["msg_z"])
|
||||||
expect(selectVisibleUserMessages(users)).toBe(users)
|
expect(selectVisibleUserMessages(users)).toBe(users)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ export function isTimelineReady(messages: Message[] | undefined, loading: boolea
|
|||||||
|
|
||||||
export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) {
|
export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) {
|
||||||
if (!revertMessageID) return messages
|
if (!revertMessageID) return messages
|
||||||
return messages.filter((message) => message.id < revertMessageID)
|
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||||
|
return boundary < 0 ? messages : messages.slice(0, boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadOlderTimeline(input: {
|
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", () => {
|
test("renders an optimistic user turn and thinking before the protocol message arrives", () => {
|
||||||
const source = [
|
const source = [
|
||||||
{ id: "msg_1", type: "user", text: "existing", time: { created: 1 } },
|
{ id: "msg_z", type: "user", text: "existing", time: { created: 1 } },
|
||||||
] satisfies SessionMessageInfo[]
|
] satisfies SessionMessageInfo[]
|
||||||
const normalized = normalizeSessionMessages("ses_1", source)
|
const normalized = normalizeSessionMessages("ses_1", source)
|
||||||
const optimistic = {
|
const optimistic = {
|
||||||
id: "msg_2",
|
id: "msg_a",
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
time: { created: 2 },
|
time: { created: 2 },
|
||||||
@@ -161,10 +161,10 @@ describe("current session timeline rows", () => {
|
|||||||
|
|
||||||
expect(result.activeMessageID).toBe(optimistic.id)
|
expect(result.activeMessageID).toBe(optimistic.id)
|
||||||
expect(result.rows.map(TimelineRow.key)).toEqual([
|
expect(result.rows.map(TimelineRow.key)).toEqual([
|
||||||
"user-message:msg_1",
|
"user-message:msg_z",
|
||||||
"turn-gap:msg_2",
|
"turn-gap:msg_a",
|
||||||
"user-message:msg_2",
|
"user-message:msg_a",
|
||||||
"thinking:msg_2",
|
"thinking:msg_a",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai
|
|||||||
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
|
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
|
||||||
import { TimelineRow, type SummaryDiff } from "./timeline-row"
|
import { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||||
import { uniqueSummaryDiffs } from "./summary-diffs"
|
import { uniqueSummaryDiffs } from "./summary-diffs"
|
||||||
|
import { compareMessages } from "@/utils/session-message"
|
||||||
|
|
||||||
export { TimelineRow, type SummaryDiff } from "./timeline-row"
|
export { TimelineRow, type SummaryDiff } from "./timeline-row"
|
||||||
|
|
||||||
@@ -71,12 +72,12 @@ export namespace Timeline {
|
|||||||
turns.push(turn)
|
turns.push(turn)
|
||||||
turnByUserID.set(user.id, turn)
|
turnByUserID.set(user.id, turn)
|
||||||
})
|
})
|
||||||
const latestUserMessageID = turns.at(-1)?.user.id
|
|
||||||
projectedUserMessages.forEach((user) => {
|
projectedUserMessages.forEach((user) => {
|
||||||
if (turnByUserID.has(user.id)) return
|
if (turnByUserID.has(user.id)) return
|
||||||
if (latestUserMessageID && user.id < latestUserMessageID) return
|
|
||||||
const turn = { user, assistants: [] }
|
const turn = { user, assistants: [] }
|
||||||
turns.push(turn)
|
const index = turns.findIndex((item) => compareMessages(user, item.user) < 0)
|
||||||
|
if (index < 0) turns.push(turn)
|
||||||
|
if (index >= 0) turns.splice(index, 0, turn)
|
||||||
turnByUserID.set(user.id, turn)
|
turnByUserID.set(user.id, turn)
|
||||||
})
|
})
|
||||||
const activeMessageID = turns.at(-1)?.user.id
|
const activeMessageID = turns.at(-1)?.user.id
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const visibleUserMessages = () => {
|
const visibleUserMessages = () => {
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
if (!revert) return userMessages()
|
if (!revert) return userMessages()
|
||||||
return userMessages().filter((m) => m.id < revert)
|
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||||
|
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAllFiles = () => {
|
const showAllFiles = () => {
|
||||||
@@ -337,7 +338,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const promptSession = prompt.capture()
|
const promptSession = prompt.capture()
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
const messages = userMessages()
|
const messages = userMessages()
|
||||||
const message = findLast(messages, (x) => !revert || x.id < revert)
|
const boundary = revert ? messages.findIndex((message) => message.id === revert) : messages.length
|
||||||
|
if (boundary < 0) return
|
||||||
|
const message = messages[boundary - 1]
|
||||||
if (!message) return
|
if (!message) return
|
||||||
const parts = sync().data.part[message.id]
|
const parts = sync().data.part[message.id]
|
||||||
|
|
||||||
@@ -352,7 +355,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
updatePrompt: (promptSession) => {
|
updatePrompt: (promptSession) => {
|
||||||
if (parts) promptSession.set(extractPromptFromParts(parts, { directory }))
|
if (parts) promptSession.set(extractPromptFromParts(parts, { directory }))
|
||||||
},
|
},
|
||||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < message.id)),
|
updateViewport: () => setActiveMessage(messages[boundary - 2]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,14 +370,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const revertMessageID = info()?.revert?.messageID
|
const revertMessageID = info()?.revert?.messageID
|
||||||
if (!revertMessageID) return
|
if (!revertMessageID) return
|
||||||
|
|
||||||
const next = messages.find((x) => x.id > revertMessageID)
|
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
||||||
|
if (boundary < 0) return
|
||||||
|
const next = messages[boundary + 1]
|
||||||
if (!next) {
|
if (!next) {
|
||||||
await runCommand({
|
await runCommand({
|
||||||
owner,
|
owner,
|
||||||
prompt: promptSession,
|
prompt: promptSession,
|
||||||
request: () => session.revert.clear({ sessionID }),
|
request: () => session.revert.clear({ sessionID }),
|
||||||
updatePrompt: (promptSession) => promptSession.reset(),
|
updatePrompt: (promptSession) => promptSession.reset(),
|
||||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)),
|
updateViewport: () => setActiveMessage(messages.at(-1)),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -384,7 +389,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
prompt: promptSession,
|
prompt: promptSession,
|
||||||
request: () => session.revert.stage({ sessionID, messageID: next.id }),
|
request: () => session.revert.stage({ sessionID, messageID: next.id }),
|
||||||
updatePrompt: () => undefined,
|
updatePrompt: () => undefined,
|
||||||
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)),
|
updateViewport: () => setActiveMessage(messages[boundary]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write
|
|||||||
const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" }
|
const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" }
|
||||||
const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
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> {
|
function record(value: unknown): value is Record<string, unknown> {
|
||||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"description": "Effect-native confined code execution over schema-described tools",
|
"description": "Effect-native confined code execution over schema-described tools",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
"go.banner.text": "يحصل GPT 5.6 Luna على حدود استخدام مضاعفة لفترة محدودة",
|
"go.banner.text": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
|
"يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||||
"go.banner.text": "GPT 5.6 Luna tem limites de uso 2x maiores por tempo limitado",
|
"go.banner.text": "DeepSeek V4 Flash tem limites de uso 2x maiores por tempo limitado",
|
||||||
"go.meta.description":
|
"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.",
|
"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",
|
"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",
|
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
"go.banner.text": "GPT 5.6 Luna får fordoblet brugsgrænse i en begrænset periode",
|
"go.banner.text": "DeepSeek V4 Flash får fordoblet brugsgrænse i en begrænset periode",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
"go.banner.text": "GPT 5.6 Luna erhält für begrenzte Zeit 2x Nutzungslimits",
|
"go.banner.text": "DeepSeek V4 Flash erhält für begrenzte Zeit 2x Nutzungslimits",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "following exceptions",
|
"zen.privacy.exceptionsLink": "following exceptions",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||||
"go.banner.text": "GPT 5.6 Luna gets 2× usage limits for a limited time",
|
"go.banner.text": "DeepSeek V4 Flash gets 2× usage limits for a limited time",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Low cost coding models for everyone",
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||||
"go.banner.text": "GPT 5.6 Luna tiene límites de uso 2x mayores por tiempo limitado",
|
"go.banner.text": "DeepSeek V4 Flash tiene límites de uso 2x mayores por tiempo limitado",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||||
"go.banner.text": "GPT 5.6 Luna bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
"go.banner.text": "DeepSeek V4 Flash bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||||
"go.banner.text": "GPT 5.6 Luna offre limiti di utilizzo 2x superiori per un periodo limitato",
|
"go.banner.text": "DeepSeek V4 Flash offre limiti di utilizzo 2x superiori per un periodo limitato",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下の例外",
|
"zen.privacy.exceptionsLink": "以下の例外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||||
"go.banner.text": "GPT 5.6 Lunaの利用上限が期間限定で2倍に",
|
"go.banner.text": "DeepSeek V4 Flashの利用上限が期間限定で2倍に",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
|
"Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
|
||||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "다음 예외",
|
"zen.privacy.exceptionsLink": "다음 예외",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||||
"go.banner.text": "GPT 5.6 Luna 사용 한도가 한시적으로 2배 확대됩니다",
|
"go.banner.text": "DeepSeek V4 Flash 사용 한도가 한시적으로 2배 확대됩니다",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
|
"Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
|
||||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||||
"go.banner.text": "GPT 5.6 Luna får 2x bruksgrense i en begrenset periode",
|
"go.banner.text": "DeepSeek V4 Flash får 2x bruksgrense i en begrenset periode",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||||
"go.banner.text": "GPT 5.6 Luna oferuje 2x wyższe limity użycia przez ograniczony czas",
|
"go.banner.text": "DeepSeek V4 Flash oferuje 2x wyższe limity użycia przez ograniczony czas",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||||
"go.banner.text": "GPT 5.6 Luna получает 2x лимиты использования на ограниченное время",
|
"go.banner.text": "DeepSeek V4 Flash получает 2x лимиты использования на ограниченное время",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
|
"Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
|
||||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
"go.banner.text": "GPT 5.6 Luna เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
|
"go.banner.text": "DeepSeek V4 Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
|
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
|
||||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||||
"go.banner.text": "GPT 5.6 Luna sınırlı bir süre için 2x kullanım limiti sunuyor",
|
"go.banner.text": "DeepSeek V4 Flash sınırlı bir süre için 2x kullanım limiti sunuyor",
|
||||||
"go.meta.description":
|
"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 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",
|
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "такими винятками",
|
"zen.privacy.exceptionsLink": "такими винятками",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||||
"go.banner.text": "GPT 5.6 Luna отримує 2x ліміти використання протягом обмеженого часу",
|
"go.banner.text": "DeepSeek V4 Flash отримує 2x ліміти використання протягом обмеженого часу",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
|
"Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
|
||||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||||
"go.banner.text": "GPT 5.6 Luna 限时享受 2 倍使用额度",
|
"go.banner.text": "DeepSeek V4 Flash 限时享受 2 倍使用额度",
|
||||||
"go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。",
|
"go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。",
|
||||||
"go.hero.title": "人人可用的低成本编程模型",
|
"go.hero.title": "人人可用的低成本编程模型",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||||
"go.banner.text": "GPT 5.6 Luna 限時享有 2 倍使用額度",
|
"go.banner.text": "DeepSeek V4 Flash 限時享有 2 倍使用額度",
|
||||||
"go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
|
"go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
|
||||||
"go.hero.title": "低成本全民編碼模型",
|
"go.hero.title": "低成本全民編碼模型",
|
||||||
"go.hero.body":
|
"go.hero.body":
|
||||||
|
|||||||
@@ -486,8 +486,8 @@ body {
|
|||||||
--bar-go: var(--color-go-2);
|
--bar-go: var(--color-go-2);
|
||||||
|
|
||||||
@media (max-width: 60rem) {
|
@media (max-width: 60rem) {
|
||||||
width: 50%;
|
width: calc(100% - 32px);
|
||||||
max-width: 50%;
|
max-width: calc(100% - 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="plot"] {
|
[data-slot="plot"] {
|
||||||
@@ -496,6 +496,10 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-left: -56px;
|
margin-left: -56px;
|
||||||
|
|
||||||
|
@media (max-width: 60rem) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="ylabels"] {
|
[data-slot="ylabels"] {
|
||||||
@@ -525,7 +529,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 60rem) {
|
@media (max-width: 60rem) {
|
||||||
&:not([data-tick="1"], [data-tick="25"], [data-tick="100"], [data-tick="250"]) {
|
&:not([data-tick="1"], [data-tick="10"], [data-tick="50"], [data-tick="250"]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -572,6 +576,17 @@ body {
|
|||||||
opacity: 0;
|
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] {
|
[data-name] {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -589,6 +604,10 @@ body {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,19 +72,27 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
|
||||||
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
|
||||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" },
|
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" },
|
||||||
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna (2x usage)", req: 4100, baseReq: 2050, d: "290ms" },
|
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 4100, baseReq: 2050, d: "290ms" },
|
||||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
||||||
{ id: "hy3", name: "Hy3", req: 4300, d: "320ms" },
|
{ id: "hy3", name: "Hy3", req: 4300, d: "320ms" },
|
||||||
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
|
||||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" },
|
{
|
||||||
|
id: "deepseek-v4-flash",
|
||||||
|
name: "DeepSeek V4 Flash",
|
||||||
|
req: 63300,
|
||||||
|
baseReq: 31650,
|
||||||
|
edge: true,
|
||||||
|
d: "340ms",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const w = 720
|
const w = 1040
|
||||||
|
const chartW = 720
|
||||||
const left = 40
|
const left = 40
|
||||||
const right = 60
|
const right = 60
|
||||||
const top = 18
|
const top = 18
|
||||||
const bottom = 44
|
const bottom = 44
|
||||||
const plot = w - left - right
|
const plot = chartW - left - right
|
||||||
|
|
||||||
const ratio = (n: number) => n / baseline
|
const ratio = (n: number) => n / baseline
|
||||||
const rmax = Math.max(1, ...graph.map((m) => ratio(m.req)))
|
const rmax = Math.max(1, ...graph.map((m) => ratio(m.req)))
|
||||||
@@ -195,10 +203,12 @@ function LimitsGraph(props: { href: string }) {
|
|||||||
data-item
|
data-item
|
||||||
data-kind="go"
|
data-kind="go"
|
||||||
data-model={m.id}
|
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}
|
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-value>{m.req.toLocaleString()}</span>
|
||||||
<span data-name>{m.name}</span>
|
<span data-name>{m.name}</span>
|
||||||
|
{m.baseReq && <span data-bonus>2x usage</span>}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ export async function handler(
|
|||||||
if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID)
|
if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (v === "$org") {
|
||||||
|
if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID.replace("wrk_", "org_"))
|
||||||
|
return
|
||||||
|
}
|
||||||
headers.set(k, v)
|
headers.set(k, v)
|
||||||
})
|
})
|
||||||
headers.delete("host")
|
headers.delete("host")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -8,4 +8,7 @@
|
|||||||
- 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.
|
- 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.
|
- 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`.
|
- 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`.
|
||||||
|
- For developer-facing terminology, prefer established usage in the target language's developer community over literal translations. Cross-check maintained Firefox, KDE, and VS Code localizations, using at least two independent corpora when available. Keep established English loanwords and acronyms instead of inventing unfamiliar terms.
|
||||||
|
- Translate whole native-menu and dialog phrases in context. Audit recurring concepts for consistency and review every exact-English value; retain it only when it is an intentional product/provider/tool name, URL, code token, keyboard legend, acronym, asset name, or established borrowing.
|
||||||
|
- Record the corpora used and flag uncertain or regional terminology in review notes.
|
||||||
- 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.
|
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.18.14",
|
"version": "1.18.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { homedir, tmpdir } from "node:os"
|
|||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { getCACertificates, setDefaultCACertificates } from "node:tls"
|
import { getCACertificates, setDefaultCACertificates } from "node:tls"
|
||||||
import type { Event } from "electron"
|
import type { Event } from "electron"
|
||||||
import { app } from "electron"
|
import { app, BrowserWindow } from "electron"
|
||||||
|
|
||||||
import { Deferred, Effect, Fiber } from "effect"
|
import { Deferred, Effect, Fiber } from "effect"
|
||||||
import contextMenu from "electron-context-menu"
|
import contextMenu from "electron-context-menu"
|
||||||
@@ -408,6 +408,15 @@ const main = Effect.gen(function* () {
|
|||||||
|
|
||||||
yield* Fiber.await(loadingTask)
|
yield* Fiber.await(loadingTask)
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform === "darwin") return
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length > 0) return
|
||||||
|
restoreMainWindows()
|
||||||
|
})
|
||||||
|
|
||||||
const windows = restoreMainWindows()
|
const windows = restoreMainWindows()
|
||||||
if (windows.length) createMenu(menuDeps)
|
if (windows.length) createMenu(menuDeps)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const dict = {
|
||||||
|
"desktop.menu.checkForUpdates": "ዝማኔዎችን ይመልከቱ...",
|
||||||
|
"desktop.menu.installCli": "ጫን CLI...",
|
||||||
|
"desktop.menu.reloadWebview": "ዳግም ጫን Webview",
|
||||||
|
"desktop.menu.restart": "ዳግም አስጀምር",
|
||||||
|
"desktop.dialog.chooseFolder": "አቃፊ ምረጥ",
|
||||||
|
"desktop.dialog.chooseFile": "ፋይል ምረጥ",
|
||||||
|
"desktop.dialog.saveFile": "ፋይሉን አስቀምጥ",
|
||||||
|
"desktop.updater.checkFailed.title": "ማዘመን ቼክ አልተሳካም",
|
||||||
|
"desktop.updater.checkFailed.message": "ዝማኔዎችን ማረጋገጥ አልተሳካም",
|
||||||
|
"desktop.updater.none.title": "ምንም ማሻሻያ የለም",
|
||||||
|
"desktop.updater.none.message": "አሁን የቅርብ ጊዜውን የOpenCode ስሪት እየተጠቀሙ ነው",
|
||||||
|
"desktop.updater.downloadFailed.title": "ዝማኔ አልተሳካም",
|
||||||
|
"desktop.updater.downloadFailed.message": "ዝማኔን ማውረድ አልተሳካም",
|
||||||
|
"desktop.updater.downloaded.title": "ዝማኔው ወርዷል",
|
||||||
|
"desktop.updater.downloaded.prompt": "ስሪት {{version}} ከOpenCode ወርዷል፣ መጫን እና እንደገና ማስጀመር ይፈልጋሉ?",
|
||||||
|
"desktop.updater.installFailed.title": "ዝማኔ አልተሳካም",
|
||||||
|
"desktop.updater.installFailed.message": "ዝማኔን መጫን አልተሳካም",
|
||||||
|
"desktop.cli.installed.title": "CLI ተጭኗል",
|
||||||
|
"desktop.cli.installed.message": "CLI ወደ {{path}} ተጭኗል\n\nየ'opencode' ትዕዛዝን ለመጠቀም ተርሚናልዎን እንደገና ያስጀምሩት።",
|
||||||
|
"desktop.cli.failed.title": "መጫኑ አልተሳካም",
|
||||||
|
"desktop.cli.failed.message": "CLIን መጫን አልተሳካም፦ {{error}}",
|
||||||
|
|
||||||
|
"desktop.error.dev.rootNotFound":
|
||||||
|
"ሥርወ አካል አልተገኘም። ወደ የእርስዎ index.html ማከልን ረስተዋል? ወይም የመታወቂያ ባህሪው የተሳሳተ ፊደል ተጽፎ ሊሆን ይችላል?",
|
||||||
|
}
|
||||||
@@ -24,4 +24,7 @@ export const dict = {
|
|||||||
"desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
|
"desktop.cli.installed.message": "تم تثبيت CLI في {{path}}\n\nأعد تشغيل الطرفية لاستخدام الأمر 'opencode'.",
|
||||||
"desktop.cli.failed.title": "فشل التثبيت",
|
"desktop.cli.failed.title": "فشل التثبيت",
|
||||||
"desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
|
"desktop.cli.failed.message": "فشل تثبيت CLI: {{error}}",
|
||||||
|
|
||||||
|
"desktop.error.dev.rootNotFound":
|
||||||
|
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ export const dict = {
|
|||||||
"CLI {{path}} ünvanına quraşdırıldı\n\n'opencode' əmrindən istifadə etmək üçün terminalı yenidən başladın.",
|
"CLI {{path}} ünvanına quraşdırıldı\n\n'opencode' əmrindən istifadə etmək üçün terminalı yenidən başladın.",
|
||||||
"desktop.cli.failed.title": "Quraşdırma uğursuz oldu",
|
"desktop.cli.failed.title": "Quraşdırma uğursuz oldu",
|
||||||
"desktop.cli.failed.message": "CLI quraşdırıla bilmədi: {{error}}",
|
"desktop.cli.failed.message": "CLI quraşdırıla bilmədi: {{error}}",
|
||||||
|
|
||||||
|
"desktop.error.dev.rootNotFound":
|
||||||
|
"Kök element tapılmadı. index.html-ə əlavə etməyi unutmusunuz? Yoxsa id atributu səhv yazılıb?",
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user