mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 642d6c52c4 |
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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,10 +28,6 @@
|
|||||||
- 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.16",
|
"version": "1.18.14",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ 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"]
|
||||||
@@ -153,67 +152,6 @@ 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,14 +379,7 @@ 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 []
|
||||||
if (results.length) {
|
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
||||||
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,8 +123,7 @@ export function SessionContextTab() {
|
|||||||
() => {
|
() => {
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
if (!revert) return userMessages()
|
if (!revert) return userMessages()
|
||||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
return userMessages().filter((m) => m.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, created = 1) =>
|
const userMessage = (id: string, sessionID: string) =>
|
||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: { created },
|
time: { created: 1 },
|
||||||
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_z", sessionID, 1), userMessage("msg_b", sessionID, 3)] },
|
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
|
||||||
part: { msg_a: [textPart("prt_1", sessionID, "msg_a")] },
|
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: { type: "message.updated", properties: { info: userMessage("msg_a", sessionID, 2) } },
|
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
|
||||||
store,
|
store,
|
||||||
setStore,
|
setStore,
|
||||||
push() {},
|
push() {},
|
||||||
@@ -384,14 +384,14 @@ describe("applyDirectoryEvent", () => {
|
|||||||
loadLsp() {},
|
loadLsp() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a", "msg_b"])
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: {
|
event: {
|
||||||
type: "message.updated",
|
type: "message.updated",
|
||||||
properties: {
|
properties: {
|
||||||
info: {
|
info: {
|
||||||
...userMessage("msg_a", sessionID, 2),
|
...userMessage("msg_2", sessionID),
|
||||||
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_a")?.role).toBe("assistant")
|
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
|
||||||
|
|
||||||
applyDirectoryEvent({
|
applyDirectoryEvent({
|
||||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_a" } },
|
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
|
||||||
store,
|
store,
|
||||||
setStore,
|
setStore,
|
||||||
push() {},
|
push() {},
|
||||||
@@ -414,8 +414,8 @@ describe("applyDirectoryEvent", () => {
|
|||||||
loadLsp() {},
|
loadLsp() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_b"])
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
|
||||||
expect(store.part.msg_a).toBeUndefined()
|
expect(store.part.msg_2).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("upserts and prunes message parts", () => {
|
test("upserts and prunes message parts", () => {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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([
|
||||||
@@ -276,7 +275,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
input.setStore("message", info.sessionID, [info])
|
input.setStore("message", info.sessionID, [info])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
const result = Binary.search(messages, info.id, (m) => m.id)
|
||||||
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
|
||||||
@@ -296,8 +295,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 index = messages.findIndex((message) => message.id === props.messageID)
|
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||||
if (index >= 0) messages.splice(index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
}
|
}
|
||||||
const parts = draft.part[props.messageID]
|
const parts = draft.part[props.messageID]
|
||||||
if (parts) {
|
if (parts) {
|
||||||
@@ -323,7 +322,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, (item) => item.id)
|
const result = Binary.search(parts, part.id, (p) => p.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
|
||||||
@@ -346,13 +345,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, (part) => part.id)
|
const result = Binary.search(parts, props.partID, (p) => p.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, (part) => part.id)
|
const next = Binary.search(list, props.partID, (p) => p.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]
|
||||||
@@ -365,7 +364,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, (part) => part.id)
|
const result = Binary.search(parts, props.partID, (p) => p.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]
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ describe("normalizeProviderList", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(result.connected).toEqual(["openai"])
|
expect(result.connected).toEqual(["openai"])
|
||||||
expect(result.defaultModel).toEqual({ providerID: "openai", modelID: "gpt-5" })
|
|
||||||
expect(result.default).toEqual({ openai: "gpt-5" })
|
expect(result.default).toEqual({ openai: "gpt-5" })
|
||||||
expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined()
|
expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined()
|
||||||
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
||||||
@@ -114,10 +113,6 @@ describe("normalizeProviderList", () => {
|
|||||||
variants: { high: {} },
|
variants: { high: {} },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves an empty current default", () => {
|
|
||||||
expect(normalizeProviderList([] as ProviderListOutput["data"], [], null).defaultModel).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("directoryKey", () => {
|
describe("directoryKey", () => {
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ export function normalizeProviderList(
|
|||||||
return {
|
return {
|
||||||
all,
|
all,
|
||||||
connected: providers.map((provider) => provider.id),
|
connected: providers.map((provider) => provider.id),
|
||||||
defaultModel: defaultModel ? { providerID: defaultModel.providerID, modelID: defaultModel.id } : null,
|
|
||||||
default: Object.fromEntries(
|
default: Object.fromEntries(
|
||||||
providers.flatMap((provider) => {
|
providers.flatMap((provider) => {
|
||||||
const model =
|
const model =
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ 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"
|
||||||
@@ -20,7 +18,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", "fa", "dv"])
|
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"])
|
||||||
|
|
||||||
function localeDirection(locale: Locale): Direction {
|
function localeDirection(locale: Locale): Direction {
|
||||||
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||||
@@ -41,7 +39,36 @@ function cookie(locale: Locale) {
|
|||||||
|
|
||||||
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
|
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
|
||||||
|
|
||||||
const INTL = DESKTOP_NATIVE_LOCALE_TAGS
|
const INTL: Record<Locale, 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",
|
||||||
|
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]])
|
||||||
@@ -77,40 +104,6 @@ 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) {
|
||||||
@@ -128,9 +121,63 @@ 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 {
|
||||||
@@ -151,17 +198,7 @@ function readStoredLocale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const warm = readStoredLocale() ?? detectLocale()
|
const warm = readStoredLocale() ?? detectLocale()
|
||||||
const initialLocale =
|
if (warm !== "en") void loadDict(warm)
|
||||||
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",
|
||||||
@@ -207,7 +244,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 = intl()
|
document.documentElement.lang = value
|
||||||
document.documentElement.dir = direction()
|
document.documentElement.dir = direction()
|
||||||
document.cookie = cookie(value)
|
document.cookie = cookie(value)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { createStore } from "solid-js/store"
|
|||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||||
@@ -150,8 +149,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const configuredModel = () => {
|
const configuredModel = () => {
|
||||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
const configured = sync().data.config.model
|
||||||
if (!model) return
|
if (!configured) return
|
||||||
|
const [providerID, modelID] = configured.split("/")
|
||||||
|
const model = { providerID, modelID }
|
||||||
if (validModel(model)) return model
|
if (validModel(model)) return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,7 +264,6 @@ 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 () => {
|
||||||
@@ -1498,7 +1497,7 @@ describe("server session", () => {
|
|||||||
|
|
||||||
await store.sync("child", { force: true })
|
await store.sync("child", { force: true })
|
||||||
|
|
||||||
expect(store.data.message.child).toEqual([older, boundary])
|
expect(store.data.message.child).toEqual([boundary, older])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves a part update for a message being loaded from history", async () => {
|
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 { compareMessages, messageKey, normalizeSessionMessages } from "@/utils/session-message"
|
import { normalizeSessionMessages } from "@/utils/session-message"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
import { 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,6 +26,7 @@ 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
|
||||||
@@ -63,7 +64,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) => compareMessages(a.info, b.info))
|
.sort((a, b) => cmp(a.info.id, b.info.id))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.info.role === "user") {
|
if (item.info.role === "user") {
|
||||||
return {
|
return {
|
||||||
@@ -110,16 +111,17 @@ 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, messageKey(item.message), messageKey)
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
const found = result.found
|
if (!result.found) session.splice(result.index, 0, item.message)
|
||||||
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 = found ? item.parts.filter((part) => current?.some((value) => value.id === part.id)) : []
|
const confirmed = result.found
|
||||||
if (found) observed.push({ messageID: item.message.id, parts: confirmed })
|
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
|
||||||
|
: []
|
||||||
|
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||||
part.set(
|
part.set(
|
||||||
item.message.id,
|
item.message.id,
|
||||||
merge(
|
merge(
|
||||||
found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||||
item.parts.filter((part) => !confirmed.includes(part)),
|
item.parts.filter((part) => !confirmed.includes(part)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -156,7 +158,6 @@ 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]))
|
||||||
@@ -179,8 +180,7 @@ 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)
|
||||||
const items = [...result.values()]
|
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
|
||||||
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,7 +413,8 @@ 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?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||||
|
load.retainedMessages.add(messageID)
|
||||||
const parts = load.touchedParts.get(messageID)
|
const parts = load.touchedParts.get(messageID)
|
||||||
if (parts) {
|
if (parts) {
|
||||||
parts.add(partID)
|
parts.add(partID)
|
||||||
@@ -436,14 +437,16 @@ 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?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||||
|
load.retainedMessages.add(messageID)
|
||||||
}
|
}
|
||||||
for (const [messageID, parts] of load.removedParts) {
|
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?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||||
|
load.retainedMessages.add(messageID)
|
||||||
}
|
}
|
||||||
for (const [messageID, parts] of load.optimisticParts) {
|
for (const [messageID, parts] of load.optimisticParts) {
|
||||||
load.removedMessages.delete(messageID)
|
load.removedMessages.delete(messageID)
|
||||||
@@ -552,7 +555,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(compareMessages),
|
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||||
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)),
|
||||||
@@ -569,7 +572,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(compareMessages),
|
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
||||||
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)),
|
||||||
@@ -693,7 +696,7 @@ export function createServerSession(
|
|||||||
const normalized = normalizeSessionMessages(sessionID, source)
|
const normalized = normalizeSessionMessages(sessionID, source)
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
session: normalized.messages.sort(compareMessages),
|
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||||
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)),
|
||||||
@@ -710,7 +713,6 @@ 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))
|
||||||
@@ -752,7 +754,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 || compareMessages(message, oldest) < 0 ? message : oldest),
|
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
if (generations.get(sessionID) !== active) return
|
if (generations.get(sessionID) !== active) return
|
||||||
@@ -802,15 +804,14 @@ 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" ||
|
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
||||||
(!result.complete && (!first || ((message: Message) => compareMessages(message, first) < 0)))
|
|
||||||
applyMessagePage(
|
applyMessagePage(
|
||||||
sessionID,
|
sessionID,
|
||||||
result,
|
result,
|
||||||
@@ -927,7 +928,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(compareMessages)
|
const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id))
|
||||||
projectV2({ sessionID, messages, touched: [message.id] })
|
projectV2({ sessionID, messages, touched: [message.id] })
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -1050,7 +1051,7 @@ export function createServerSession(
|
|||||||
setData("message", info.sessionID, [info])
|
setData("message", info.sessionID, [info])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
const result = Binary.search(messages, info.id, (message) => message.id)
|
||||||
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
||||||
if (!result.found)
|
if (!result.found)
|
||||||
setData("message", info.sessionID, (value = []) => {
|
setData("message", info.sessionID, (value = []) => {
|
||||||
@@ -1083,8 +1084,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 index = messages.findIndex((message) => message.id === props.messageID)
|
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
||||||
if (index >= 0) messages.splice(index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
}
|
}
|
||||||
deleteMessageParts(draft, props.messageID)
|
deleteMessageParts(draft, props.messageID)
|
||||||
}),
|
}),
|
||||||
@@ -1096,7 +1097,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?.some((message) => message.id === part.messageID)
|
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
|
||||||
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
||||||
if (
|
if (
|
||||||
missing &&
|
missing &&
|
||||||
@@ -1340,7 +1341,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]).sort(compareMessages))
|
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
||||||
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, created = 1): Message => ({
|
const userMessage = (id: string, sessionID: string): Message => ({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: { created },
|
time: { created: 1 },
|
||||||
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 by creation time", () => {
|
test("applyOptimisticAdd inserts message in sorted order and stores parts", () => {
|
||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const draft = {
|
const draft = {
|
||||||
message: { [sessionID]: [userMessage("msg_z", sessionID, 1)] },
|
message: { [sessionID]: [userMessage("msg_2", sessionID)] },
|
||||||
part: {} as Record<string, Part[] | undefined>,
|
part: {} as Record<string, Part[] | undefined>,
|
||||||
}
|
}
|
||||||
|
|
||||||
applyOptimisticAdd(draft, {
|
applyOptimisticAdd(draft, {
|
||||||
sessionID,
|
sessionID,
|
||||||
message: userMessage("msg_a", sessionID, 2),
|
message: userMessage("msg_1", sessionID),
|
||||||
parts: [textPart("prt_2", sessionID, "msg_a"), textPart("prt_1", sessionID, "msg_a")],
|
parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||||
expect(draft.part.msg_a?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applyOptimisticRemove removes message and part entries", () => {
|
test("applyOptimisticRemove removes message and part entries", () => {
|
||||||
@@ -60,33 +60,19 @@ describe("sync optimistic reducers", () => {
|
|||||||
const sessionID = "ses_1"
|
const sessionID = "ses_1"
|
||||||
const page = mergeOptimisticPage(
|
const page = mergeOptimisticPage(
|
||||||
{
|
{
|
||||||
session: [userMessage("msg_z", sessionID, 1)],
|
session: [userMessage("msg_1", sessionID)],
|
||||||
part: [{ id: "msg_z", part: [textPart("prt_1", sessionID, "msg_z")] }],
|
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
||||||
complete: true,
|
complete: true,
|
||||||
},
|
},
|
||||||
[{ message: userMessage("msg_a", sessionID, 2), parts: [textPart("prt_2", sessionID, "msg_a")] }],
|
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(page.session.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||||
expect(page.part.find((x) => x.id === "msg_a")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
expect(page.part.find((x) => x.id === "msg_2")?.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,7 +3,6 @@ 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"])
|
||||||
|
|
||||||
@@ -68,7 +67,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, messageKey(item.message), messageKey)
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
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)
|
||||||
|
|
||||||
@@ -93,7 +92,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, messageKey(input.message), messageKey)
|
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||||
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]
|
||||||
@@ -104,8 +103,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 index = messages.findIndex((message) => message.id === input.messageID)
|
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||||
if (index >= 0) messages.splice(index, 1)
|
if (result.found) messages.splice(result.index, 1)
|
||||||
}
|
}
|
||||||
delete draft.part[input.messageID]
|
delete draft.part[input.messageID]
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-28
@@ -3,7 +3,6 @@
|
|||||||
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"
|
||||||
@@ -150,31 +149,29 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (root instanceof HTMLElement) {
|
if (root instanceof HTMLElement) {
|
||||||
void loadInitialLocale().then((locale) => {
|
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||||
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
clearAuthToken()
|
||||||
clearAuthToken()
|
const server: ServerConnection.Http = {
|
||||||
const server: ServerConnection.Http = {
|
type: "http",
|
||||||
type: "http",
|
authToken: !!auth,
|
||||||
authToken: !!auth,
|
http: {
|
||||||
http: {
|
url: getCurrentUrl(),
|
||||||
url: getCurrentUrl(),
|
...auth,
|
||||||
...auth,
|
},
|
||||||
},
|
}
|
||||||
}
|
render(
|
||||||
render(
|
() => (
|
||||||
() => (
|
<PlatformProvider value={platform}>
|
||||||
<PlatformProvider value={platform}>
|
<AppBaseProviders>
|
||||||
<AppBaseProviders locale={locale}>
|
<AppInterface
|
||||||
<AppInterface
|
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||||
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
canonicalLocalServer={ServerConnection.key(server)}
|
||||||
canonicalLocalServer={ServerConnection.key(server)}
|
servers={[server]}
|
||||||
servers={[server]}
|
disableHealthCheck
|
||||||
disableHealthCheck
|
/>
|
||||||
/>
|
</AppBaseProviders>
|
||||||
</AppBaseProviders>
|
</PlatformProvider>
|
||||||
</PlatformProvider>
|
),
|
||||||
),
|
root,
|
||||||
root,
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { resolveDefaultModel, selectProviderCatalog } from "./provider-catalog"
|
import { selectProviderCatalog } from "./provider-catalog"
|
||||||
|
|
||||||
const catalog = (id: string): NormalizedProviderListResponse => ({
|
const catalog = (id: string): NormalizedProviderListResponse => ({
|
||||||
all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]),
|
all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]),
|
||||||
@@ -57,21 +57,3 @@ test("falls back to the global catalog for route consumers", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(global)
|
).toBe(global)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses the current server default model", () => {
|
|
||||||
expect(resolveDefaultModel({ providerID: "openai", modelID: "gpt-5" }, "anthropic/claude")).toEqual({
|
|
||||||
providerID: "openai",
|
|
||||||
modelID: "gpt-5",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not use legacy config when the current server has no default", () => {
|
|
||||||
expect(resolveDefaultModel(null, "anthropic/claude")).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("uses config for legacy servers", () => {
|
|
||||||
expect(resolveDefaultModel(undefined, "anthropic/claude")).toEqual({
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -25,13 +25,3 @@ export function selectProviderCatalog(input: ProviderCatalogInput) {
|
|||||||
if (input.explicit) return emptyProviderCatalog
|
if (input.explicit) return emptyProviderCatalog
|
||||||
return input.global
|
return input.global
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDefaultModel(
|
|
||||||
current: NormalizedProviderListResponse["defaultModel"],
|
|
||||||
legacy: string | undefined,
|
|
||||||
) {
|
|
||||||
if (current !== undefined) return current ?? undefined
|
|
||||||
if (!legacy) return undefined
|
|
||||||
const [providerID, modelID] = legacy.split("/")
|
|
||||||
return { providerID, modelID }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export function useProviders(directory: Accessor<string | undefined>) {
|
|||||||
return {
|
return {
|
||||||
all: () => providers().all,
|
all: () => providers().all,
|
||||||
default: () => providers().default,
|
default: () => providers().default,
|
||||||
defaultModel: () => providers().defaultModel,
|
|
||||||
popular: () =>
|
popular: () =>
|
||||||
pipe(
|
pipe(
|
||||||
providers().all,
|
providers().all,
|
||||||
|
|||||||
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,8 +5,6 @@ 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,
|
||||||
@@ -43,40 +41,6 @@ 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",
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -110,42 +74,3 @@ 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,40 +27,6 @@ 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]
|
||||||
@@ -94,131 +60,6 @@ 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,5 +1,4 @@
|
|||||||
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",
|
||||||
@@ -29,51 +28,19 @@ 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 = new Map(
|
const pluralCategories: Partial<Record<(typeof appLocales)[number], readonly string[]>> = {
|
||||||
appLocales.map(
|
ar: ["zero", "two", "few", "many"],
|
||||||
(locale) =>
|
br: ["many"],
|
||||||
[
|
bs: ["few"],
|
||||||
locale,
|
es: ["many"],
|
||||||
desktopNativePluralCategories(locale).filter((category) => category !== "one" && category !== "other"),
|
fr: ["many"],
|
||||||
] as const,
|
it: ["many"],
|
||||||
),
|
pl: ["few", "many"],
|
||||||
)
|
ru: ["few", "many"],
|
||||||
|
uk: ["few", "many"],
|
||||||
|
}
|
||||||
|
|
||||||
const domains = [
|
const domains = [
|
||||||
{
|
{
|
||||||
@@ -107,7 +74,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.get(locale) ?? []).map((category) => `${key}.${category}`))
|
.flatMap((key) => (pluralCategories[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,
|
||||||
@@ -128,7 +95,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.get(locale) ?? [])
|
(pluralCategories[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()),
|
||||||
)
|
)
|
||||||
@@ -177,12 +144,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.get(locale) ?? [])
|
(pluralCategories[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.get(locale) ?? [])
|
(pluralCategories[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": "输入 token 的大致细分。“其他”包含工具定义和开销。",
|
"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": "总 token",
|
"context.stats.totalTokens": "令牌总数",
|
||||||
"context.stats.usage": "使用率",
|
"context.stats.usage": "使用率",
|
||||||
"context.stats.inputTokens": "输入 token",
|
"context.stats.inputTokens": "输入令牌",
|
||||||
"context.stats.outputTokens": "输出 token",
|
"context.stats.outputTokens": "输出令牌",
|
||||||
"context.stats.reasoningTokens": "推理 token",
|
"context.stats.reasoningTokens": "推理令牌",
|
||||||
"context.stats.cacheTokens": "缓存 token(读/写)",
|
"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": "Token",
|
"context.usage.tokens": "令牌数",
|
||||||
"context.usage.usage": "使用率",
|
"context.usage.usage": "使用率",
|
||||||
"context.usage.cost": "成本",
|
"context.usage.cost": "成本",
|
||||||
"context.usage.clickToView": "点击查看上下文",
|
"context.usage.clickToView": "点击查看上下文",
|
||||||
|
|||||||
@@ -90,20 +90,7 @@ 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) => {
|
directories.forEach((item) => ctx.projects.open(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,10 +475,6 @@ 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 { compareSessionTime, displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
import { 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(compareSessionTime)
|
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||||
.flatMap((session) => {
|
.flatMap((session) => {
|
||||||
const directory = pathKey(session.directory)
|
const directory = pathKey(session.directory)
|
||||||
const project =
|
const project =
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
|
|||||||
import {
|
import {
|
||||||
childSessionOnPath,
|
childSessionOnPath,
|
||||||
closeHomeProject,
|
closeHomeProject,
|
||||||
compareSessionTime,
|
|
||||||
displayName,
|
displayName,
|
||||||
effectiveWorkspaceOrder,
|
effectiveWorkspaceOrder,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -19,7 +18,6 @@ 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"
|
||||||
@@ -155,30 +153,6 @@ 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,10 +9,18 @@ type SessionStore = {
|
|||||||
path: { directory: string }
|
path: { directory: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compareSessionTime(a: Session, b: Session) {
|
function sortSessions(now: number) {
|
||||||
const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)
|
const oneMinuteAgo = now - 60 * 1000
|
||||||
if (updated !== 0) return updated
|
return (a: Session, b: Session) => {
|
||||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
const aUpdated = a.time.updated ?? a.time.created
|
||||||
|
const bUpdated = b.time.updated ?? b.time.created
|
||||||
|
const aRecent = aUpdated > oneMinuteAgo
|
||||||
|
const bRecent = bUpdated > oneMinuteAgo
|
||||||
|
if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||||
|
if (aRecent && !bRecent) return -1
|
||||||
|
if (!aRecent && bRecent) return 1
|
||||||
|
return bUpdated - aUpdated
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||||
@@ -21,10 +29,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(compareSessionTime)
|
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||||
|
|
||||||
export const latestRootSession = (stores: SessionStore[], _now: number) =>
|
export const latestRootSession = (stores: SessionStore[], now: number) =>
|
||||||
stores.flatMap(roots).sort(compareSessionTime)[0]
|
stores.flatMap(roots).sort(sortSessions(now))[0]
|
||||||
|
|
||||||
export function hasProjectPermissions<T>(
|
export function hasProjectPermissions<T>(
|
||||||
request: Record<string, T[] | undefined> | undefined,
|
request: Record<string, T[] | undefined> | undefined,
|
||||||
|
|||||||
@@ -1851,9 +1851,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const session = sdk().api.session
|
const session = sdk().api.session
|
||||||
const target = sync()
|
const target = sync()
|
||||||
const index = userMessages().findIndex((item) => item.id === id)
|
const next = userMessages().find((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({
|
||||||
@@ -1893,10 +1891,8 @@ 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()
|
||||||
.slice(index)
|
.filter((item) => item.id >= id)
|
||||||
.map((item) => ({ id: item.id, text: line(item.id) }))
|
.map((item) => ({ id: item.id, text: line(item.id) }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { usePrompt } from "@/context/prompt"
|
|||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
|
||||||
|
|
||||||
export function createPromptModelSelection(input: { agent: () => { model?: ModelKey; variant?: string } | undefined }) {
|
export function createPromptModelSelection(input: { agent: () => { model?: ModelKey; variant?: string } | undefined }) {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -22,8 +21,10 @@ export function createPromptModelSelection(input: { agent: () => { model?: Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configured = () => {
|
const configured = () => {
|
||||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
const value = sync().data.config.model
|
||||||
if (!model) return
|
if (!value) return
|
||||||
|
const [providerID, modelID] = value.split("/")
|
||||||
|
const model = { providerID, modelID }
|
||||||
if (valid(model)) return model
|
if (valid(model)) return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,9 +287,7 @@ 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] ?? []
|
||||||
if (!boundary) return messages
|
return boundary ? messages.filter((message) => message.id < boundary) : 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_z"), assistant("msg_a"), user("msg_b"), user("msg_c")]
|
const messages: Message[] = [user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_5")]
|
||||||
const users = selectUserMessages(messages)
|
const users = selectUserMessages(messages)
|
||||||
|
|
||||||
expect(users.map((message) => message.id)).toEqual(["msg_z", "msg_b", "msg_c"])
|
expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"])
|
||||||
expect(selectVisibleUserMessages(users, "msg_b").map((message) => message.id)).toEqual(["msg_z"])
|
expect(selectVisibleUserMessages(users, "msg_5").map((message) => message.id)).toEqual(["msg_1", "msg_3"])
|
||||||
expect(selectVisibleUserMessages(users)).toBe(users)
|
expect(selectVisibleUserMessages(users)).toBe(users)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ 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
|
||||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
return messages.filter((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_z", type: "user", text: "existing", time: { created: 1 } },
|
{ id: "msg_1", 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_a",
|
id: "msg_2",
|
||||||
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_z",
|
"user-message:msg_1",
|
||||||
"turn-gap:msg_a",
|
"turn-gap:msg_2",
|
||||||
"user-message:msg_a",
|
"user-message:msg_2",
|
||||||
"thinking:msg_a",
|
"thinking:msg_2",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai
|
|||||||
import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part"
|
import { 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"
|
||||||
|
|
||||||
@@ -72,12 +71,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: [] }
|
||||||
const index = turns.findIndex((item) => compareMessages(user, item.user) < 0)
|
turns.push(turn)
|
||||||
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,8 +100,7 @@ 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()
|
||||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
return userMessages().filter((m) => m.id < revert)
|
||||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAllFiles = () => {
|
const showAllFiles = () => {
|
||||||
@@ -338,9 +337,7 @@ 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 boundary = revert ? messages.findIndex((message) => message.id === revert) : messages.length
|
const message = findLast(messages, (x) => !revert || x.id < revert)
|
||||||
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]
|
||||||
|
|
||||||
@@ -355,7 +352,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(messages[boundary - 2]),
|
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < message.id)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,16 +367,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const revertMessageID = info()?.revert?.messageID
|
const revertMessageID = info()?.revert?.messageID
|
||||||
if (!revertMessageID) return
|
if (!revertMessageID) return
|
||||||
|
|
||||||
const boundary = messages.findIndex((message) => message.id === revertMessageID)
|
const next = messages.find((x) => x.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(messages.at(-1)),
|
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -389,7 +384,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(messages[boundary]),
|
updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write
|
|||||||
const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" }
|
const 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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"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": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة",
|
"go.banner.text": "يحصل GPT 5.6 Luna على حدود استخدام مضاعفة لفترة محدودة",
|
||||||
"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": "DeepSeek V4 Flash tem limites de uso 2x maiores por tempo limitado",
|
"go.banner.text": "GPT 5.6 Luna tem limites de uso 2x maiores por tempo limitado",
|
||||||
"go.meta.description":
|
"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": "DeepSeek V4 Flash får fordoblet brugsgrænse i en begrænset periode",
|
"go.banner.text": "GPT 5.6 Luna får fordoblet brugsgrænse i en begrænset periode",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash erhält für begrenzte Zeit 2x Nutzungslimits",
|
"go.banner.text": "GPT 5.6 Luna erhält für begrenzte Zeit 2x Nutzungslimits",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash gets 2× usage limits for a limited time",
|
"go.banner.text": "GPT 5.6 Luna gets 2× usage limits for a limited time",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash tiene límites de uso 2x mayores por tiempo limitado",
|
"go.banner.text": "GPT 5.6 Luna tiene límites de uso 2x mayores por tiempo limitado",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
"go.banner.text": "GPT 5.6 Luna bénéficie de limites d’utilisation 2x supérieures pour une durée limitée",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash offre limiti di utilizzo 2x superiori per un periodo limitato",
|
"go.banner.text": "GPT 5.6 Luna offre limiti di utilizzo 2x superiori per un periodo limitato",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flashの利用上限が期間限定で2倍に",
|
"go.banner.text": "GPT 5.6 Lunaの利用上限が期間限定で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": "DeepSeek V4 Flash 사용 한도가 한시적으로 2배 확대됩니다",
|
"go.banner.text": "GPT 5.6 Luna 사용 한도가 한시적으로 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": "DeepSeek V4 Flash får 2x bruksgrense i en begrenset periode",
|
"go.banner.text": "GPT 5.6 Luna får 2x bruksgrense i en begrenset periode",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash oferuje 2x wyższe limity użycia przez ograniczony czas",
|
"go.banner.text": "GPT 5.6 Luna oferuje 2x wyższe limity użycia przez ograniczony czas",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash получает 2x лимиты использования на ограниченное время",
|
"go.banner.text": "GPT 5.6 Luna получает 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": "DeepSeek V4 Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
|
"go.banner.text": "GPT 5.6 Luna เพิ่มโควตาการใช้งานเป็น 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": "DeepSeek V4 Flash sınırlı bir süre için 2x kullanım limiti sunuyor",
|
"go.banner.text": "GPT 5.6 Luna sınırlı bir süre için 2x kullanım limiti sunuyor",
|
||||||
"go.meta.description":
|
"go.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": "DeepSeek V4 Flash отримує 2x ліміти використання протягом обмеженого часу",
|
"go.banner.text": "GPT 5.6 Luna отримує 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": "DeepSeek V4 Flash 限时享受 2 倍使用额度",
|
"go.banner.text": "GPT 5.6 Luna 限时享受 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": "DeepSeek V4 Flash 限時享有 2 倍使用額度",
|
"go.banner.text": "GPT 5.6 Luna 限時享有 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: calc(100% - 32px);
|
width: 50%;
|
||||||
max-width: calc(100% - 32px);
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="plot"] {
|
[data-slot="plot"] {
|
||||||
@@ -496,10 +496,6 @@ 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"] {
|
||||||
@@ -529,7 +525,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 60rem) {
|
@media (max-width: 60rem) {
|
||||||
&:not([data-tick="1"], [data-tick="10"], [data-tick="50"], [data-tick="250"]) {
|
&:not([data-tick="1"], [data-tick="25"], [data-tick="100"], [data-tick="250"]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,17 +572,6 @@ 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;
|
||||||
@@ -604,10 +589,6 @@ 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,27 +72,19 @@ 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", req: 4100, baseReq: 2050, d: "290ms" },
|
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna (2x usage)", req: 4100, baseReq: 2050, d: "290ms" },
|
||||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
|
{ id: "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 = 1040
|
const w = 720
|
||||||
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 = chartW - left - right
|
const plot = w - 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)))
|
||||||
@@ -203,12 +195,10 @@ 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,10 +238,6 @@ 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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"$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.16",
|
"version": "1.18.14",
|
||||||
"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.16",
|
"version": "1.18.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user