Compare commits

...

55 Commits

Author SHA1 Message Date
opencode-agent[bot] fe82a1b6ca chore: generate 2026-08-08 03:39:08 +00:00
Brendan Allan 2ea728f731 fix(app): populate project picker from home (#41158) 2026-08-08 11:37:30 +08:00
Aarav Sareen 284214c78d feat(app): open project menu on right-click (#41013) 2026-08-07 17:25:46 +08:00
日常无语的无语者 b05a0d1fbe fix(i18n): use 词元 instead of 令牌 for token in zh locale (#40977) 2026-08-07 19:22:23 +10:00
Jack be25c905fa docs(zen): replace Ling Flash with Tiny (#41027) 2026-08-07 16:48:42 +08:00
Brendan Allan 5347b5e009 fix(app): register new home projects (#41018) 2026-08-07 07:34:32 +00:00
opencode-agent[bot] b467518dae fix(desktop): preserve macOS app on window close (#40974)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-08-07 07:25:12 +00:00
opencode 1ec6bdc8c6 sync release versions for v1.18.15 2026-08-07 06:49:53 +00:00
Aarav Sareen 265472ef72 fix(ui): use tokens for button loading state (#40884) 2026-08-07 12:11:00 +05:30
Frank 325529761b sync 2026-08-07 02:22:25 -04:00
opencode-agent[bot] 741244b69d feat(app): add global locale coverage (#40992)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-07 05:41:11 +00:00
Jack 4abddbbfdd feat(go): promote DeepSeek V4 Flash usage (#40988) 2026-08-07 13:24:03 +08:00
opencode-agent[bot] 5aa5cb3523 fix(app): use chronological message boundaries (#41006)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 05:23:28 +00:00
opencode-agent[bot] 9113255114 fix(app): order stored messages by creation time (#41001)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 05:06:28 +00:00
opencode-agent[bot] 23cc677108 fix(tui): order messages by creation time (#40994)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 04:41:15 +00:00
opencode-agent[bot] 28bcc0e4f4 fix(app): sort sessions by persisted time (#41000)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 04:23:04 +00:00
Dmitry Nefedov 8bf5062b89 feat(tui): add cursor style configuration (#32295)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-06 23:20:51 -05:00
opencode-agent[bot] 20750c332e fix(web): order shared messages by creation time (#40995)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 04:11:28 +00:00
opencode-agent[bot] db581e47a3 fix(opencode): order legacy message loop by time (#40990)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 04:08:23 +00:00
opencode-agent[bot] a54a693af2 fix(opencode): use chronological message boundaries (#40991)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 04:08:02 +00:00
opencode-agent[bot] d468201952 fix(opencode): use file times for truncation cleanup (#40987)
Co-authored-by: Dax <mail@thdxr.com>
2026-08-07 03:58:31 +00:00
opencode-agent[bot] 31c5c4e924 fix(app): keep locale names native (#40985)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-07 03:32:43 +00:00
opencode-agent[bot] 8d65dbdd04 fix(app): complete translation coverage (#40981)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-08-07 03:06:23 +00:00
opencode-agent[bot] b7f9363393 fix(opencode): serialize orphaned compaction history (#40800)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-06 21:11:14 -05:00
opencode-agent[bot] 69f2cbaa3a chore: generate 2026-08-06 23:37:25 +00:00
Frank dc898ca2c3 sync 2026-08-06 19:34:26 -04:00
Adam 03bff6500a fix(app): update homepage stats 2026-08-06 11:07:33 -05:00
Adam b379d71d19 fix(stats): update github stars 2026-08-06 10:58:58 -05:00
ayu def7220bfc fix(tui): support copying over ssh with set-clipboard on tmux config (#30472)
Co-authored-by: Simon Klee <hello@simonklee.dk>
2026-08-06 10:23:32 +02:00
opencode-agent[bot] f6c5afe5a9 fix(desktop): disable packaged console logging (#40794)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-06 17:25:41 +10:00
Frank 847771fe06 fix log processor 2026-08-06 03:21:16 -04:00
opencode-agent[bot] b8bd88901a chore: generate 2026-08-06 01:52:15 +00:00
Luke Parker f1adabcddc feat(app): export session as json from ui (#40781) 2026-08-06 11:50:33 +10:00
Luke Parker 24470e52a5 fix(desktop): embed version in server sidecar (#40764) 2026-08-06 07:22:11 +08:00
mridul 23bbc5cd14 fix(server): allow blob attachments in web UI (#40692) 2026-08-05 22:23:32 +00:00
opencode ebf6fc07a1 sync release versions for v1.18.14 2026-08-05 20:58:52 +00:00
opencode-agent[bot] 04513d9692 chore: generate 2026-08-05 20:56:57 +00:00
Adam 146720e197 fix(stats): improve page speed 2026-08-05 15:54:35 -05:00
opencode-agent[bot] b1f8cc04af chore: update nix node_modules hashes 2026-08-05 20:33:25 +00:00
opencode-agent[bot] 082fe93e16 chore: generate 2026-08-05 20:16:40 +00:00
Aiden Cline 709c195905 fix(opencode): preserve compatible stream errors (#40718) 2026-08-05 15:14:03 -05:00
opencode-agent[bot] 3355b78d91 chore: generate 2026-08-05 20:06:05 +00:00
Adam b84c63d034 fix(stats): reduce html payloads 2026-08-05 15:02:39 -05:00
opencode-agent[bot] 057b5a9dee chore: generate 2026-08-05 19:41:32 +00:00
Aiden Cline 61aefc0759 fix(opencode): expand retryable error patterns (#40707) 2026-08-05 14:39:49 -05:00
Aiden Cline f929f8f100 refactor(opencode): simplify retry error matching (#40694) 2026-08-05 13:05:19 -05:00
opencode-agent[bot] 4a57013cf8 fix(app): show pending tool details (#40603)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-05 08:25:06 +00:00
Jack 2f17fc9613 docs(zen): add LongCat free model (#40585) 2026-08-05 12:49:28 +08:00
opencode-agent[bot] b8ea3ea091 chore: generate 2026-08-05 02:33:08 +00:00
Luke Parker 82a5796159 test(app): harden flaky e2e synchronization (#40556) 2026-08-05 12:31:45 +10:00
opencode-agent[bot] 9f38562237 fix(opencode): include cache writes in ACP usage (#40450)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-04 21:27:01 -05:00
opencode-agent[bot] 5b4fb1f770 chore: generate 2026-08-05 02:20:04 +00:00
Aiden Cline cb88db6ce3 tweak(opencode): make xAI OAuth device-only to reduce confusion w/ headless environments (#40537) 2026-08-04 21:18:44 -05:00
Luke Parker 66fdd51f0d docs: add RTL development skill (#40543) 2026-08-05 01:01:10 +00:00
opencode-agent[bot] 98dd65cd60 fix(app): collapse deletion-only edit parts (#40536)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-05 00:23:06 +00:00
340 changed files with 50509 additions and 1778 deletions
+1
View File
@@ -322,6 +322,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
env: env:
NODE_OPTIONS: --max-old-space-size=4096 NODE_OPTIONS: --max-old-space-size=4096
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_ORG: ${{ vars.SENTRY_ORG }}
@@ -0,0 +1,63 @@
---
name: rtl-aware-development
description: OpenCode Desktop should be RTL-aware. Use when implementing or reviewing RTL/LTR behavior in the web app, desktop app, CSS, menus, scrolling, resizing, icons, mixed-direction text, or Electron title bars.
---
# RTL-Aware Development
Treat direction as independent from language. Test English in both directions as well as real RTL and mixed-script content.
## Guidelines
- Set `lang` and `dir` on the document, and propagate direction through component providers used by portaled menus and popovers. Do not change the selected locale merely to force RTL.
- Keep DOM and focus order semantic. Flexbox and Grid already follow `dir`; do not add `row-reverse`, CSS `order`, or reversed markup just to mirror a layout.
- Prefer logical CSS for semantic layout. Reserve physical coordinates for pointer positions, canvas geometry, native window controls, and other genuinely physical placement.
```css
/* Avoid */
padding-left: 12px;
right: 0;
border-right: 1px solid;
text-align: left;
/* Prefer */
padding-inline-start: 12px;
inset-inline-end: 0;
border-inline-end: 1px solid;
text-align: start;
```
- Isolate mixed-direction text. Use `dir="auto"` or `<bdi>` for unknown text; keep code, URLs, IDs, and filesystem paths LTR without forcing the surrounding component LTR.
```html
<span class="file-row"><bdi dir="auto">README.md</bdi></span> <bdi dir="ltr"><code>C:\src\app.ts</code></bdi>
```
- Mirror directional meaning, not every image. Back/forward, previous/next, disclosure, indentation, and directional progress may need mirroring. Do not mirror brands, clocks, media controls, charts, or text. Reverse physical gradients, `translateX`, SVG transforms, and animation deltas explicitly.
- Map interactions through direction. `clientX` remains physical; resizing a logical edge needs an RTL-aware delta. Logical previous/next keyboard controls may swap ArrowLeft/ArrowRight. Follow the relevant WAI-ARIA widget pattern.
- Do not assume LTR scrolling. RTL `scrollLeft` can start at `0` and become negative. Prefer `scrollIntoView({ inline: "nearest" })` or a tested direction-normalizing helper.
- For Electron title bars, prefer native caption controls and use `titleBarOverlay` plus `env(titlebar-area-*)` for the safe content rectangle. Keep Windows/macOS native-control avoidance and `trafficLightPosition` physical; keep app navigation inside that rectangle logical. Mark interactive titlebar children `app-region: no-drag`.
- Verify behavior, not screenshots alone. Check computed styles, pseudo-element geometry, hit zones, focus order, keyboard behavior, submenu direction, zoom/scaling, and both LTR and RTL scroll endpoints.
## Test Matrix
- English + LTR
- English + forced RTL
- A real RTL locale + RTL
- Mixed RTL/LTR content, long labels, numbers, code, and paths
- Keyboard, pointer resize, scrolling, menus/submenus, and Electron titlebar controls in both directions
## References
- [RTL Styling 101, Ahmad Shadeed](https://rtlstyling.com/posts/rtl-styling/)
- [CSS-Tricks: RTL Styling 101](https://css-tricks.com/rtl-styling-101/)
- [CSS-Tricks: CSS Logical Properties and Values](https://css-tricks.com/css-logical-properties-and-values/)
- [W3C: Structural markup and right-to-left text](https://www.w3.org/International/questions/qa-html-dir)
- [W3C: Inline bidirectional markup](https://www.w3.org/International/articles/inline-bidi-markup/)
- [MDN: CSS logical properties and values](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Logical_properties_and_values)
- [MDN: `dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir)
- [MDN: `scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft)
- [web.dev: Logical properties](https://web.dev/learn/css/logical-properties/)
- [Electron: Custom title bar](https://www.electronjs.org/docs/latest/tutorial/custom-title-bar)
- [WAI-ARIA: Window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
- [Kobalte: I18n Provider](https://kobalte.dev/docs/core/components/i18n-provider/)
+29 -28
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@corvu/drawer": "catalog:", "@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0", "@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.18.13", "version": "1.18.15",
"bin": { "bin": {
"lildax": "./bin/lildax.cjs", "lildax": "./bin/lildax.cjs",
}, },
@@ -144,7 +144,7 @@
}, },
"packages/codemode": { "packages/codemode": {
"name": "@opencode-ai/codemode", "name": "@opencode-ai/codemode",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"acorn": "8.15.0", "acorn": "8.15.0",
"effect": "catalog:", "effect": "catalog:",
@@ -158,7 +158,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -194,7 +194,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -221,7 +221,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.82", "@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -243,7 +243,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -267,7 +267,7 @@
}, },
"packages/console/support": { "packages/console/support": {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-core": "workspace:*",
@@ -287,7 +287,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.18.13", "version": "1.18.15",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -381,7 +381,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
@@ -435,7 +435,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -449,7 +449,7 @@
}, },
"packages/effect-sqlite-node": { "packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
}, },
@@ -461,7 +461,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@hono/standard-validator": "catalog:", "@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -493,7 +493,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -509,7 +509,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83",
@@ -540,7 +540,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
@@ -559,7 +559,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.18.13", "version": "1.18.15",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -690,7 +690,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -766,7 +766,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -781,7 +781,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@opencode-ai/server", "name": "@opencode-ai/server",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*", "@opencode-ai/protocol": "workspace:*",
@@ -796,7 +796,7 @@
}, },
"packages/session-ui": { "packages/session-ui": {
"name": "@opencode-ai/session-ui", "name": "@opencode-ai/session-ui",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -836,7 +836,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -849,7 +849,7 @@
}, },
"packages/stats/app": { "packages/stats/app": {
"name": "@opencode-ai/stats-app", "name": "@opencode-ai/stats-app",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
@@ -883,7 +883,7 @@
}, },
"packages/stats/core": { "packages/stats/core": {
"name": "@opencode-ai/stats-core", "name": "@opencode-ai/stats-core",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@aws-sdk/client-athena": "3.933.0", "@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0", "@planetscale/database": "1.19.0",
@@ -902,7 +902,7 @@
}, },
"packages/stats/server": { "packages/stats/server": {
"name": "@opencode-ai/stats-server", "name": "@opencode-ai/stats-server",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@aws-sdk/client-firehose": "3.933.0", "@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -944,7 +944,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@opencode-ai/tui", "name": "@opencode-ai/tui",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
@@ -971,7 +971,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
@@ -1022,7 +1022,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.18.13", "version": "1.18.15",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -1078,6 +1078,7 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"pacote@21.5.0": "patches/pacote@21.5.0.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
}, },
"overrides": { "overrides": {
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=", "x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=", "aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=", "aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY=" "x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
} }
} }
+2 -1
View File
@@ -158,6 +158,7 @@
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch"
} }
} }
+7
View File
@@ -24,7 +24,14 @@
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors. - NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change. - When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it. - NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts.
- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code.
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`. - 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
+16
View File
@@ -0,0 +1,16 @@
## Required Reading
- Before writing, changing, or reviewing E2E tests, ALWAYS read and follow Playwright's official [Best Practices](https://playwright.dev/docs/best-practices), [Auto-waiting](https://playwright.dev/docs/actionability), and [Assertions](https://playwright.dev/docs/test-assertions) guides.
- Use the official [Locators](https://playwright.dev/docs/locators), [Network](https://playwright.dev/docs/network), and [Test Isolation](https://playwright.dev/docs/browser-contexts) guides when those concerns apply.
## Test Hygiene
- Test user-visible behavior with isolated, deterministic data and scoped, unique locators.
- Prefer role, label, text, and explicit test-contract locators. Do not use `.first()` or `.last()` merely to silence strictness errors.
- Use locator actions, Playwright auto-waiting, and web-first assertions for observable readiness and outcomes.
- NEVER use `waitForTimeout`, `setTimeout`, sleeps, animation-frame counts, or other wall-clock delays to synchronize a test. Wait for the specific UI state, request, response, event, or application outcome instead.
- Do not treat navigation, a network response, DOM attachment, or visibility alone as proof that asynchronously rendered UI is ready. Assert the state the next action actually requires.
- Register event and network waits before the action that triggers them.
- Do not retry state-changing actions. Retry idempotent readiness checks, then perform the action once and assert its outcome.
- Keep action and assertion timeouts adaptive. Do not use short timeouts as readiness probes or rely on retries to hide flakes.
- Assert exact outcomes and identities so stale state, duplicate rendering, and interactions with the wrong element cannot pass.
@@ -197,7 +197,9 @@ export async function setupTimeline(
) )
}, },
async waitForPart(partID: string) { async waitForPart(partID: string) {
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible() const part = page.locator(`[data-timeline-part-id="${partID}"]`)
await expect(part).toHaveCount(1)
await expect(part).toBeVisible()
}, },
} }
} }
@@ -18,6 +18,7 @@ test("opens the comment editor when code is clicked", async ({ page }) => {
await line.click() await line.click()
await expect(review.getByRole("textbox")).toBeVisible() await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
}) })
test("opens the comment editor when a line number is clicked", async ({ page }) => { test("opens the comment editor when a line number is clicked", async ({ page }) => {
@@ -27,6 +28,7 @@ test("opens the comment editor when a line number is clicked", async ({ page })
await lineNumber.click() await lineNumber.click()
await expect(review.getByRole("textbox")).toBeVisible() await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
}) })
test("opens the comment editor for a line number range", async ({ page }) => { test("opens the comment editor for a line number range", async ({ page }) => {
@@ -36,15 +38,10 @@ test("opens the comment editor for a line number range", async ({ page }) => {
await expectAppVisible(start) await expectAppVisible(start)
await expectAppVisible(end) await expectAppVisible(end)
const from = await start.boundingBox() await start.dragTo(end)
const to = await end.boundingBox()
if (!from || !to) throw new Error("Missing line number bounds")
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2)
await page.mouse.down()
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2)
await page.mouse.up()
await expect(review.getByRole("textbox")).toBeVisible() await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on lines 1-3")
}) })
test("shows a comment button when a line number is hovered", async ({ page }) => { test("shows a comment button when a line number is hovered", async ({ page }) => {
@@ -54,31 +51,38 @@ test("shows a comment button when a line number is hovered", async ({ page }) =>
const comment = review.getByRole("button", { name: "Comment", exact: true }) const comment = review.getByRole("button", { name: "Comment", exact: true })
await expect(async () => { await expect(async () => {
await page.mouse.move(0, 0)
await lineNumber.hover() await lineNumber.hover()
await expect(comment).toBeVisible({ timeout: 500 }) await expect(lineNumber).toHaveAttribute("data-hovered", "")
await comment.click({ timeout: 500 }) await expect(comment).toHaveCount(1)
}).toPass() await expect(comment).toHaveCSS("pointer-events", "auto")
await comment.focus()
await expect(comment).toBeFocused()
}).toPass({ timeout: 10_000 })
await comment.press("Enter")
await expect(review.getByRole("textbox")).toBeVisible() await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
}) })
test("stages a submitted line comment in the prompt context", async ({ page }) => { test("stages a submitted line comment in the prompt context", async ({ page }) => {
const requests: string[] = []
page.on("request", (request) => { page.on("request", (request) => {
if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`) expect.soft(request.method(), `unexpected ${request.method()} ${new URL(request.url()).pathname}`).toBe("GET")
}) })
const review = page.locator('[data-component="session-review"]') const review = page.locator('[data-component="session-review"]')
await review.getByText("export const value = 'after'", { exact: true }).click() await review.getByText("export const value = 'after'", { exact: true }).click()
await review.getByRole("textbox").fill("Use the existing value instead") const textbox = review.getByRole("textbox")
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click() await expect(textbox).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
await textbox.fill("Use the existing value instead")
const submit = review.locator('[data-slot="line-comment-action"][data-variant="primary"]')
await expect(submit).toBeEnabled()
await submit.click()
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible() await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "Session" }).click() await page.getByRole("tab", { name: "Session" }).click()
const context = page.getByText("Use the existing value instead", { exact: true }).last() const context = page.getByText("Use the existing value instead", { exact: true }).last()
await expect(context).toBeVisible() await expect(context).toBeVisible()
await expect(context.locator("..")).toContainText("review.ts:2") await expect(context.locator("..")).toContainText("review.ts:2")
expect(requests).toEqual([])
}) })
async function openReview(page: Page) { async function openReview(page: Page) {
@@ -144,15 +148,22 @@ async function openReview(page: Page) {
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title) await expectSessionTitle(page, title)
const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff") const changes = page.getByRole("tab", { name: "Changes" })
await page.getByRole("tab", { name: "Changes" }).click() const diffResponse = page.waitForResponse(
(response) =>
response.request().method() === "GET" && response.ok() && new URL(response.url()).pathname === "/api/vcs/diff",
)
await changes.click()
expect((await (await diffResponse).json()).data).toHaveLength(1) expect((await (await diffResponse).json()).data).toHaveLength(1)
await expect(page.getByRole("tab", { selected: true })).toHaveAccessibleName(/Files Changed/)
const review = page.locator('[data-component="session-review"]') const review = page.locator('[data-component="session-review"]')
await expectAppVisible(review) await expectAppVisible(review)
await review const file = review.locator('[data-file="src/review.ts"]')
.getByRole("heading", { name: /review\.ts/ }) await expectAppVisible(file)
.getByRole("button") const trigger = file.getByRole("button", { expanded: false })
.first() await expect(trigger).toHaveCount(1)
.click() await trigger.click()
await expect(file.getByRole("button", { expanded: true })).toBeVisible()
await expect(file.getByText("export const value = 'after'", { exact: true })).toBeVisible()
} }
@@ -1,9 +1,8 @@
import { expect, test } from "@playwright/test" import { expect, test, type Page } from "@playwright/test"
import { import {
assistantMessage, assistantMessage,
partUpdated, partUpdated,
setupTimeline, setupTimeline,
status,
textPart, textPart,
userMessage, userMessage,
} from "../performance/timeline-stability/fixture" } from "../performance/timeline-stability/fixture"
@@ -17,7 +16,7 @@ test("keeps one connection open while delivering multiple events", async ({ page
await timeline.waitForPart("prt_transport_first") await timeline.waitForPart("prt_transport_first")
await timeline.waitForPart("prt_transport_second") await timeline.waitForPart("prt_transport_second")
expect(first.connectionID).toBe(second.connectionID) expect(first.connectionID).toBe(second.connectionID)
expect(await timeline.transport.connections()).toHaveLength(1) await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
expect(await timeline.transport.acknowledgements()).toHaveLength(2) expect(await timeline.transport.acknowledgements()).toHaveLength(2)
}) })
@@ -51,20 +50,28 @@ test("parses split JSON and a split multibyte code point", async ({ page }) => {
}) })
test("delivers server heartbeat without mutating the timeline", async ({ page }) => { test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
const sentinelID = "prt_transport_heartbeat_sentinel"
const timeline = await setupTimeline(page, { const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])], messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
}) })
const before = await page.locator("[data-timeline-row]").allTextContents() await timeline.waitForPart("prt_transport_steady")
const before = await stableTimelineRows(page)
await timeline.transport.heartbeat() await timeline.transport.writeRaw(": heartbeat\n\n")
await timeline.settle() await timeline.transport.send(partUpdated(textPart(sentinelID, "heartbeat processed")))
await timeline.waitForPart(sentinelID)
expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before) await expect
expect(await timeline.transport.connections()).toHaveLength(1) .poll(async () => {
const rows = await timelineRows(page)
return rows.filter((row) => before.some((item) => item.key === row.key))
})
.toEqual(before)
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
}) })
test("reconnects after a clean close", async ({ page }) => { test("reconnects after a clean close", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 }) const timeline = await setupTimeline(page)
const first = await timeline.transport.waitForConnection() const first = await timeline.transport.waitForConnection()
await timeline.transport.close() await timeline.transport.close()
@@ -77,20 +84,21 @@ test("reconnects after a clean close", async ({ page }) => {
}) })
test("reconnects after a stream error", async ({ page }) => { test("reconnects after a stream error", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 }) const timeline = await setupTimeline(page)
const first = await timeline.transport.waitForConnection() const first = await timeline.transport.waitForConnection()
await timeline.transport.error("contract failure") await timeline.transport.error("contract failure")
const second = await timeline.transport.waitForConnection({ after: first.id }) const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(status("busy")) await timeline.transport.send(partUpdated(textPart("prt_transport_error", "after error")))
await timeline.waitForPart("prt_transport_error")
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2) await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
expect(second.id).toBeGreaterThan(first.id) expect(second.id).toBeGreaterThan(first.id)
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error") expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
}) })
test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" }) const timeline = await setupTimeline(page, { protocol: "v2" })
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
id: "timeline-event-7", id: "timeline-event-7",
}) })
@@ -112,5 +120,35 @@ test("passes through non-event fetches", async ({ page }) => {
}) })
expect(health).toEqual({ healthy: true }) expect(health).toEqual({ healthy: true })
expect(await timeline.transport.connections()).toHaveLength(1) await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
}) })
async function stableTimelineRows(page: Page) {
let previous: Awaited<ReturnType<typeof timelineRows>> | undefined
let stable = 0
await expect
.poll(
async () => {
const next = await timelineRows(page)
stable = JSON.stringify(next) === JSON.stringify(previous) ? stable + 1 : 0
previous = next
return stable
},
{ intervals: [50, 50, 100] },
)
.toBeGreaterThanOrEqual(2)
return previous!
}
function timelineRows(page: Page) {
return page.locator("[data-timeline-key]").evaluateAll((elements) =>
elements.map((element) => ({
key: element.getAttribute("data-timeline-key"),
row: element.querySelector("[data-timeline-row]")?.getAttribute("data-timeline-row"),
parts: Array.from(element.querySelectorAll("[data-timeline-part-id]"), (part) =>
part.getAttribute("data-timeline-part-id"),
),
text: element.textContent,
})),
)
}
+10 -5
View File
@@ -247,18 +247,23 @@ export async function installSseTransport<T>(
return { return {
server, server,
async waitForConnection(input = {}) { async waitForConnection(input = {}) {
await page.waitForFunction( const connection = await page.waitForFunction(
(after) => { (after) => {
const transport = (window as BrowserTransport).__testSseTransport const transport = (window as BrowserTransport).__testSseTransport
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
return connections?.some((connection) => connection.id > after) return connections?.findLast((connection) => connection.id > after && connection.endedAt === undefined)
}, },
input.after ?? 0, input.after ?? 0,
{ timeout: input.timeout }, { timeout: input.timeout },
) )
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast( let result: SseConnectionRecord | undefined
(connection) => connection.id > (input.after ?? 0), try {
)! result = await connection.jsonValue()
} finally {
await connection.dispose()
}
if (!result) throw new Error("SSE transport connection disappeared while waiting")
return result
}, },
send(payload, eventOptions) { send(payload, eventOptions) {
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false }) return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.18.13", "version": "1.18.15",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -1,4 +1,5 @@
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
@@ -14,6 +15,7 @@ export type DialogGoUpsellProps = {
export function DialogUsageExceeded(props: DialogGoUpsellProps) { export function DialogUsageExceeded(props: DialogGoUpsellProps) {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage()
const platform = usePlatform() const platform = usePlatform()
const runAction = () => { const runAction = () => {
@@ -32,7 +34,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3"> <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button variant="ghost" size="large" onClick={dismiss}> <Button variant="ghost" size="large" onClick={dismiss}>
Don't show again {language.t("dialog.usageExceeded.dontShowAgain")}
</Button> </Button>
<Button variant="primary" size="large" onClick={runAction}> <Button variant="primary" size="large" onClick={runAction}>
{props.actionLabel} {props.actionLabel}
@@ -139,6 +139,7 @@ test("resolves directory autocomplete from the current browser root", async () =
directories.push(input.location?.directory ?? "") directories.push(input.location?.directory ?? "")
return Promise.resolve({ data: [] }) return Promise.resolve({ data: [] })
}, },
list: () => Promise.resolve({ data: [] }),
}, },
}, },
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"] } as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
@@ -152,6 +153,67 @@ test("resolves directory autocomplete from the current browser root", async () =
expect(directories).toEqual(["/repo", "/repo/src"]) expect(directories).toEqual(["/repo", "/repo/src"])
}) })
test("keeps indexed directory results for servers that support empty search", async () => {
const sdk = {
api: {
file: {
find: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
list: () => Promise.reject(new Error("listing should not run when search returns results")),
},
},
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
expect(await search("")).toEqual(["/home/luke/projects"])
})
test("lists the default directory when empty search is unsupported", async () => {
const calls: string[] = []
const directories = Array.from({ length: 60 }, (_, index) => ({
path: `project-${index}/`,
type: "directory" as const,
}))
const sdk = {
api: {
file: {
find: () => Promise.resolve({ data: [] }),
list: (input: { location?: { directory?: string } }) => {
calls.push(input.location?.directory ?? "")
return Promise.resolve({
data: [...directories, { path: "README.md", type: "file" }],
})
},
},
},
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
const results = await search("")
expect(results).toHaveLength(60)
expect(results.at(-1)).toBe("/home/luke/project-59")
expect(calls).toEqual(["/home/luke"])
})
test("matches the default directory listing when typed search is unsupported", async () => {
const sdk = {
api: {
file: {
find: () => Promise.resolve({ data: [] }),
list: () =>
Promise.resolve({
data: [
{ path: "Documents/", type: "directory" },
{ path: "Downloads/", type: "directory" },
],
}),
},
},
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
expect(await search("documents")).toEqual(["/home/luke/Documents"])
})
test("searches from an absolute root without a default base", async () => { test("searches from an absolute root without a default base", async () => {
const directories: string[] = [] const directories: string[] = []
const sdk = { const sdk = {
@@ -379,7 +379,14 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
.then((result) => result.data.map((entry) => entry.path)) .then((result) => result.data.map((entry) => entry.path))
.catch(() => []) .catch(() => [])
if (!active()) return [] if (!active()) return []
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50) if (results.length) {
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
}
const fallback = query
? await match(input.directory, query, 50)
: (await directories(input.directory)).map((item) => item.absolute)
if (!active()) return []
return fallback
} }
const segments = query.replace(/^\/+/, "").split("/") const segments = query.replace(/^\/+/, "").split("/")
const head = segments.slice(0, -1).filter((part) => part && part !== ".") const head = segments.slice(0, -1).filter((part) => part && part !== ".")
@@ -5,12 +5,15 @@ import { checksum } from "@opencode-ai/core/util/encode"
import { findLast } from "@opencode-ai/core/util/array" import { findLast } from "@opencode-ai/core/util/array"
import { same } from "@/utils/same" import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { Accordion } from "@opencode-ai/ui/accordion" import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { File } from "@opencode-ai/session-ui/file" import { File } from "@opencode-ai/session-ui/file"
import { Markdown } from "@opencode-ai/session-ui/markdown" import { Markdown } from "@opencode-ai/session-ui/markdown"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast"
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
@@ -120,7 +123,8 @@ export function SessionContextTab() {
() => { () => {
const revert = info()?.revert?.messageID const revert = info()?.revert?.messageID
if (!revert) return userMessages() if (!revert) return userMessages()
return userMessages().filter((m) => m.id < revert) const boundary = userMessages().findIndex((message) => message.id === revert)
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
}, },
emptyUserMessages, emptyUserMessages,
{ equals: same }, { equals: same },
@@ -220,6 +224,31 @@ export function SessionContextTab() {
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
] satisfies { label: string; value: () => JSX.Element }[] ] satisfies { label: string; value: () => JSX.Element }[]
const exportSession = async () => {
const sessionID = params.id
if (!sessionID) return
try {
const data = await fetchSessionExport({
sessionID,
client: sdk().client,
})
const filename = sessionExportFilename(data.info)
downloadSessionExport(filename, data)
showToast({
variant: "success",
icon: "circle-check",
title: language.t("toast.session.export.success.title"),
description: language.t("toast.session.export.success.description", { filename }),
})
} catch (err) {
showToast({
variant: "error",
title: language.t("toast.session.export.failed.title"),
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
})
}
}
let scroll: HTMLDivElement | undefined let scroll: HTMLDivElement | undefined
let frame: number | undefined let frame: number | undefined
let pending: { x: number; y: number } | undefined let pending: { x: number; y: number } | undefined
@@ -328,7 +357,18 @@ export function SessionContextTab() {
</Show> </Show>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div> <div class="flex items-center justify-between">
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
<Button
size="small"
variant="ghost"
class="gap-1.5 px-2 text-text-weak hover:text-text-base"
onClick={exportSession}
>
<Icon name="download" size="small" />
<span>{language.t("context.export.session")}</span>
</Button>
</div>
<Accordion multiple> <Accordion multiple>
<For each={messages()}> <For each={messages()}>
{(message) => ( {(message) => (
+1 -1
View File
@@ -628,7 +628,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
aria-label={props.state.ariaLabel} aria-label={props.state.ariaLabel}
> >
<span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0"> <span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0">
Update {props.state.label}
</span> </span>
<span class="flex size-5 shrink-0 items-center justify-center"> <span class="flex size-5 shrink-0 items-center justify-center">
<Show <Show
@@ -15,12 +15,12 @@ const rootSession = (input: { id: string; parentID?: string; archived?: number }
}, },
}) as Session }) as Session
const userMessage = (id: string, sessionID: string) => const userMessage = (id: string, sessionID: string, created = 1) =>
({ ({
id, id,
sessionID, sessionID,
role: "user", role: "user",
time: { created: 1 }, time: { created },
agent: "assistant", agent: "assistant",
model: { providerID: "openai", modelID: "gpt" }, model: { providerID: "openai", modelID: "gpt" },
}) as Message }) as Message
@@ -370,13 +370,13 @@ describe("applyDirectoryEvent", () => {
const sessionID = "ses_1" const sessionID = "ses_1"
const [store, setStore] = createStore( const [store, setStore] = createStore(
baseState({ baseState({
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] }, message: { [sessionID]: [userMessage("msg_z", sessionID, 1), userMessage("msg_b", sessionID, 3)] },
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] }, part: { msg_a: [textPart("prt_1", sessionID, "msg_a")] },
}), }),
) )
applyDirectoryEvent({ applyDirectoryEvent({
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } }, event: { type: "message.updated", properties: { info: userMessage("msg_a", sessionID, 2) } },
store, store,
setStore, setStore,
push() {}, push() {},
@@ -384,14 +384,14 @@ describe("applyDirectoryEvent", () => {
loadLsp() {}, loadLsp() {},
}) })
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"]) expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a", "msg_b"])
applyDirectoryEvent({ applyDirectoryEvent({
event: { event: {
type: "message.updated", type: "message.updated",
properties: { properties: {
info: { info: {
...userMessage("msg_2", sessionID), ...userMessage("msg_a", sessionID, 2),
role: "assistant", role: "assistant",
} as Message, } as Message,
}, },
@@ -403,10 +403,10 @@ describe("applyDirectoryEvent", () => {
loadLsp() {}, loadLsp() {},
}) })
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant") expect(store.message[sessionID]?.find((x) => x.id === "msg_a")?.role).toBe("assistant")
applyDirectoryEvent({ applyDirectoryEvent({
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } }, event: { type: "message.removed", properties: { sessionID, messageID: "msg_a" } },
store, store,
setStore, setStore,
push() {}, push() {},
@@ -414,8 +414,8 @@ describe("applyDirectoryEvent", () => {
loadLsp() {}, loadLsp() {},
}) })
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"]) expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_b"])
expect(store.part.msg_2).toBeUndefined() expect(store.part.msg_a).toBeUndefined()
}) })
test("upserts and prunes message parts", () => { test("upserts and prunes message parts", () => {
@@ -15,6 +15,7 @@ import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim" import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache" import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs" import { diffs as list, message as clean } from "@/utils/diffs"
import { messageKey } from "@/utils/session-message"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const SESSION_CONTENT_EVENTS = new Set([ const SESSION_CONTENT_EVENTS = new Set([
@@ -275,7 +276,7 @@ export function applyDirectoryEvent(input: {
input.setStore("message", info.sessionID, [info]) input.setStore("message", info.sessionID, [info])
break break
} }
const result = Binary.search(messages, info.id, (m) => m.id) const result = Binary.search(messages, messageKey(info), messageKey)
if (result.found) { if (result.found) {
input.setStore("message", info.sessionID, result.index, reconcile(info)) input.setStore("message", info.sessionID, result.index, reconcile(info))
break break
@@ -295,8 +296,8 @@ export function applyDirectoryEvent(input: {
produce((draft) => { produce((draft) => {
const messages = draft.message[props.sessionID] const messages = draft.message[props.sessionID]
if (messages) { if (messages) {
const result = Binary.search(messages, props.messageID, (m) => m.id) const index = messages.findIndex((message) => message.id === props.messageID)
if (result.found) messages.splice(result.index, 1) if (index >= 0) messages.splice(index, 1)
} }
const parts = draft.part[props.messageID] const parts = draft.part[props.messageID]
if (parts) { if (parts) {
@@ -322,7 +323,7 @@ export function applyDirectoryEvent(input: {
input.setStore("part", part.messageID, [part]) input.setStore("part", part.messageID, [part])
break break
} }
const result = Binary.search(parts, part.id, (p) => p.id) const result = Binary.search(parts, part.id, (item) => item.id)
if (result.found) { if (result.found) {
input.setStore("part", part.messageID, result.index, reconcile(part)) input.setStore("part", part.messageID, result.index, reconcile(part))
break break
@@ -345,13 +346,13 @@ export function applyDirectoryEvent(input: {
) )
const parts = input.store.part[props.messageID] const parts = input.store.part[props.messageID]
if (!parts) break if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id) const result = Binary.search(parts, props.partID, (part) => part.id)
if (result.found) { if (result.found) {
input.setStore( input.setStore(
produce((draft) => { produce((draft) => {
const list = draft.part[props.messageID] const list = draft.part[props.messageID]
if (!list) return if (!list) return
const next = Binary.search(list, props.partID, (p) => p.id) const next = Binary.search(list, props.partID, (part) => part.id)
if (!next.found) return if (!next.found) return
list.splice(next.index, 1) list.splice(next.index, 1)
if (list.length === 0) delete draft.part[props.messageID] if (list.length === 0) delete draft.part[props.messageID]
@@ -364,7 +365,7 @@ export function applyDirectoryEvent(input: {
const props = event.properties as { messageID: string; partID: string; field: string; delta: string } const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
const parts = input.store.part[props.messageID] const parts = input.store.part[props.messageID]
if (!parts) break if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id) const result = Binary.search(parts, props.partID, (part) => part.id)
if (!result.found) break if (!result.found) break
const field = props.field as keyof (typeof parts)[number] const field = props.field as keyof (typeof parts)[number]
const current = parts[result.index]?.[field] const current = parts[result.index]?.[field]
+53 -127
View File
@@ -8,8 +8,11 @@ import { dict as en } from "@/i18n/en"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en" import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { import {
createDesktopNativeBundle, createDesktopNativeBundle,
detectDesktopNativeLocale,
DESKTOP_NATIVE_ENGLISH, DESKTOP_NATIVE_ENGLISH,
DESKTOP_NATIVE_LABELS,
DESKTOP_NATIVE_LOCALES, DESKTOP_NATIVE_LOCALES,
DESKTOP_NATIVE_LOCALE_TAGS,
type DesktopNativeBundle, type DesktopNativeBundle,
type DesktopNativeLocale, type DesktopNativeLocale,
} from "@/i18n/desktop-native" } from "@/i18n/desktop-native"
@@ -17,7 +20,7 @@ import {
export type Locale = DesktopNativeLocale export type Locale = DesktopNativeLocale
export type Direction = "ltr" | "rtl" export type Direction = "ltr" | "rtl"
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"]) const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"])
function localeDirection(locale: Locale): Direction { function localeDirection(locale: Locale): Direction {
return RTL_LOCALES.has(locale) ? "rtl" : "ltr" return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
@@ -38,70 +41,7 @@ function cookie(locale: Locale) {
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
const INTL: Record<Locale, string> = { const INTL = DESKTOP_NATIVE_LOCALE_TAGS
en: "en",
zh: "zh-Hans",
zht: "zh-Hant",
ko: "ko",
de: "de",
es: "es",
fr: "fr",
da: "da",
ja: "ja",
pl: "pl",
ru: "ru",
uk: "uk",
ar: "ar",
no: "nb-NO",
br: "pt-BR",
th: "th",
bs: "bs",
tr: "tr",
hi: "hi-IN",
nl: "nl-NL",
id: "id-ID",
vi: "vi-VN",
it: "it-IT",
ur: "ur-PK",
pa: "pa-Arab-PK",
az: "az-Latn-AZ",
fi: "fi-FI",
sv: "sv-SE",
}
const LABEL_KEY: Partial<Record<Locale, keyof Dictionary>> = {
en: "language.en",
zh: "language.zh",
zht: "language.zht",
ko: "language.ko",
de: "language.de",
es: "language.es",
fr: "language.fr",
da: "language.da",
ja: "language.ja",
pl: "language.pl",
ru: "language.ru",
uk: "language.uk",
ar: "language.ar",
no: "language.no",
br: "language.br",
th: "language.th",
bs: "language.bs",
tr: "language.tr",
}
const LABEL: Partial<Record<Locale, string>> = {
hi: "हिन्दी",
nl: "Nederlands",
id: "Bahasa Indonesia",
vi: "Tiếng Việt",
it: "Italiano",
ur: "اردو",
pa: "پنجابی",
az: "Azərbaycanca",
fi: "Suomi",
sv: "Svenska",
}
const base = i18n.flatten({ ...en, ...uiEn }) const base = i18n.flatten({ ...en, ...uiEn })
const dicts = new Map<Locale, Dictionary>([["en", base]]) const dicts = new Map<Locale, Dictionary>([["en", base]])
@@ -137,6 +77,40 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")), az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")),
fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")), fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")),
sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")), sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")),
am: () => merge(import("@/i18n/am"), import("@opencode-ai/ui/i18n/am")),
bg: () => merge(import("@/i18n/bg"), import("@opencode-ai/ui/i18n/bg")),
bn: () => merge(import("@/i18n/bn"), import("@opencode-ai/ui/i18n/bn")),
ca: () => merge(import("@/i18n/ca"), import("@opencode-ai/ui/i18n/ca")),
cs: () => merge(import("@/i18n/cs"), import("@opencode-ai/ui/i18n/cs")),
dv: () => merge(import("@/i18n/dv"), import("@opencode-ai/ui/i18n/dv")),
dz: () => merge(import("@/i18n/dz"), import("@opencode-ai/ui/i18n/dz")),
el: () => merge(import("@/i18n/el"), import("@opencode-ai/ui/i18n/el")),
et: () => merge(import("@/i18n/et"), import("@opencode-ai/ui/i18n/et")),
fa: () => merge(import("@/i18n/fa"), import("@opencode-ai/ui/i18n/fa")),
fo: () => merge(import("@/i18n/fo"), import("@opencode-ai/ui/i18n/fo")),
hr: () => merge(import("@/i18n/hr"), import("@opencode-ai/ui/i18n/hr")),
hu: () => merge(import("@/i18n/hu"), import("@opencode-ai/ui/i18n/hu")),
hy: () => merge(import("@/i18n/hy"), import("@opencode-ai/ui/i18n/hy")),
is: () => merge(import("@/i18n/is"), import("@opencode-ai/ui/i18n/is")),
ka: () => merge(import("@/i18n/ka"), import("@opencode-ai/ui/i18n/ka")),
km: () => merge(import("@/i18n/km"), import("@opencode-ai/ui/i18n/km")),
lo: () => merge(import("@/i18n/lo"), import("@opencode-ai/ui/i18n/lo")),
lt: () => merge(import("@/i18n/lt"), import("@opencode-ai/ui/i18n/lt")),
lv: () => merge(import("@/i18n/lv"), import("@opencode-ai/ui/i18n/lv")),
mk: () => merge(import("@/i18n/mk"), import("@opencode-ai/ui/i18n/mk")),
mn: () => merge(import("@/i18n/mn"), import("@opencode-ai/ui/i18n/mn")),
ms: () => merge(import("@/i18n/ms"), import("@opencode-ai/ui/i18n/ms")),
my: () => merge(import("@/i18n/my"), import("@opencode-ai/ui/i18n/my")),
ne: () => merge(import("@/i18n/ne"), import("@opencode-ai/ui/i18n/ne")),
ro: () => merge(import("@/i18n/ro"), import("@opencode-ai/ui/i18n/ro")),
si: () => merge(import("@/i18n/si"), import("@opencode-ai/ui/i18n/si")),
sk: () => merge(import("@/i18n/sk"), import("@opencode-ai/ui/i18n/sk")),
sl: () => merge(import("@/i18n/sl"), import("@opencode-ai/ui/i18n/sl")),
sq: () => merge(import("@/i18n/sq"), import("@opencode-ai/ui/i18n/sq")),
sr: () => merge(import("@/i18n/sr"), import("@opencode-ai/ui/i18n/sr")),
tg: () => merge(import("@/i18n/tg"), import("@opencode-ai/ui/i18n/tg")),
tk: () => merge(import("@/i18n/tk"), import("@opencode-ai/ui/i18n/tk")),
uz: () => merge(import("@/i18n/uz"), import("@opencode-ai/ui/i18n/uz")),
} }
function loadDict(locale: Locale) { function loadDict(locale: Locale) {
@@ -154,63 +128,9 @@ export function loadLocaleDict(locale: Locale) {
return loadDict(locale).then(() => undefined) return loadDict(locale).then(() => undefined)
} }
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
{ locale: "en", match: (language) => language.startsWith("en") },
{
locale: "zht",
match: (language) =>
language.startsWith("zh") &&
(language.includes("hant") || language.includes("-tw") || language.includes("-hk") || language.includes("-mo")),
},
{ locale: "zh", match: (language) => language.startsWith("zh") },
{ locale: "ko", match: (language) => language.startsWith("ko") },
{ locale: "de", match: (language) => language.startsWith("de") },
{ locale: "es", match: (language) => language.startsWith("es") },
{ locale: "fr", match: (language) => language.startsWith("fr") },
{ locale: "da", match: (language) => language.startsWith("da") },
{ locale: "ja", match: (language) => language.startsWith("ja") },
{ locale: "pl", match: (language) => language.startsWith("pl") },
{ locale: "ru", match: (language) => language.startsWith("ru") },
{ locale: "uk", match: (language) => language.startsWith("uk") },
{ locale: "ar", match: (language) => language.startsWith("ar") },
{
locale: "no",
match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"),
},
{ locale: "br", match: (language) => language.startsWith("pt") },
{ locale: "th", match: (language) => language.startsWith("th") },
{ locale: "bs", match: (language) => language.startsWith("bs") },
{ locale: "tr", match: (language) => language.startsWith("tr") },
{ locale: "hi", match: (language) => language.startsWith("hi") },
{ locale: "nl", match: (language) => language.startsWith("nl") },
{ locale: "id", match: (language) => language.startsWith("id") },
{ locale: "vi", match: (language) => language.startsWith("vi") },
{ locale: "it", match: (language) => language.startsWith("it") },
{ locale: "ur", match: (language) => language.startsWith("ur") },
{
locale: "pa",
match: (language) => language.startsWith("pa") && (language.includes("arab") || language.includes("-pk")),
},
{
locale: "az",
match: (language) => language.startsWith("az") && !language.includes("arab") && !language.includes("cyrl"),
},
{ locale: "fi", match: (language) => language.startsWith("fi") },
{ locale: "sv", match: (language) => language.startsWith("sv") },
]
function detectLocale(): Locale { function detectLocale(): Locale {
if (typeof navigator !== "object") return "en" if (typeof navigator !== "object") return "en"
return detectDesktopNativeLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
for (const language of languages) {
if (!language) continue
const normalized = language.toLowerCase()
const match = localeMatchers.find((entry) => entry.match(normalized))
if (match) return match.locale
}
return "en"
} }
export function normalizeLocale(value: string): Locale { export function normalizeLocale(value: string): Locale {
@@ -231,7 +151,17 @@ function readStoredLocale() {
} }
const warm = readStoredLocale() ?? detectLocale() const warm = readStoredLocale() ?? detectLocale()
if (warm !== "en") void loadDict(warm) const initialLocale =
warm === "en"
? Promise.resolve(warm)
: loadDict(warm).then(
() => warm,
() => "en" as const,
)
export function loadInitialLocale() {
return initialLocale
}
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
name: "Language", name: "Language",
@@ -272,16 +202,12 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count }) return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
} }
const label = (value: Locale) => { const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
const key = LABEL_KEY[value]
if (key) return t(key)
return LABEL[value] ?? value
}
createEffect(() => { createEffect(() => {
if (typeof document !== "object") return if (typeof document !== "object") return
const value = locale() const value = locale()
document.documentElement.lang = value document.documentElement.lang = intl()
document.documentElement.dir = direction() document.documentElement.dir = direction()
document.cookie = cookie(value) document.cookie = cookie(value)
}) })
@@ -264,6 +264,7 @@ describe("server session", () => {
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }]) expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id]) expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
}) })
test("extends a current page to include the user for split assistant turns", async () => { test("extends a current page to include the user for split assistant turns", async () => {
@@ -1497,7 +1498,7 @@ describe("server session", () => {
await store.sync("child", { force: true }) await store.sync("child", { force: true })
expect(store.data.message.child).toEqual([boundary, older]) expect(store.data.message.child).toEqual([older, boundary])
}) })
test("preserves a part update for a message being loaded from history", async () => { test("preserves a part update for a message being loaded from history", async () => {
+28 -29
View File
@@ -18,7 +18,7 @@ import { message as cleanMessage } from "@/utils/diffs"
import { sessionNotFoundError } from "@/utils/server-errors" import { sessionNotFoundError } from "@/utils/server-errors"
import { rootSession } from "@/utils/session-route" import { rootSession } from "@/utils/session-route"
import { normalizeSessionInfo } from "@/utils/session" import { normalizeSessionInfo } from "@/utils/session"
import { normalizeSessionMessages } from "@/utils/session-message" import { compareMessages, messageKey, normalizeSessionMessages } from "@/utils/session-message"
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache" import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer" import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
import type { ServerApi } from "@/utils/server" import type { ServerApi } from "@/utils/server"
@@ -26,7 +26,6 @@ import type { ServerApi } from "@/utils/server"
type MessageApi = ServerApi["message"] type MessageApi = ServerApi["message"]
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const initialMessagePageSize = 20 const initialMessagePageSize = 20
const historyMessagePageSize = 200 const historyMessagePageSize = 200
@@ -64,7 +63,7 @@ type MessagePage = {
function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] { function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
return items return items
.slice() .slice()
.sort((a, b) => cmp(a.info.id, b.info.id)) .sort((a, b) => compareMessages(a.info, b.info))
.map((item) => { .map((item) => {
if (item.info.role === "user") { if (item.info.role === "user") {
return { return {
@@ -111,17 +110,16 @@ function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
const part = new Map(page.part.map((item) => [item.id, item.part])) const part = new Map(page.part.map((item) => [item.id, item.part]))
const observed: { messageID: string; parts: Part[] }[] = [] const observed: { messageID: string; parts: Part[] }[] = []
for (const item of items) { for (const item of items) {
const result = Binary.search(session, item.message.id, (message) => message.id) const result = Binary.search(session, messageKey(item.message), messageKey)
if (!result.found) session.splice(result.index, 0, item.message) const found = result.found
if (!found) session.splice(result.index, 0, item.message)
const current = part.get(item.message.id) const current = part.get(item.message.id)
const confirmed = result.found const confirmed = found ? item.parts.filter((part) => current?.some((value) => value.id === part.id)) : []
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found) if (found) observed.push({ messageID: item.message.id, parts: confirmed })
: []
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
part.set( part.set(
item.message.id, item.message.id,
merge( merge(
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []), found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
item.parts.filter((part) => !confirmed.includes(part)), item.parts.filter((part) => !confirmed.includes(part)),
), ),
) )
@@ -158,6 +156,7 @@ function reconcileFetched<T extends { id: string }>(
retained?: ReadonlySet<string> retained?: ReadonlySet<string>
removed?: ReadonlySet<string> removed?: ReadonlySet<string>
preserveUnfetched?: boolean | ((item: T) => boolean) preserveUnfetched?: boolean | ((item: T) => boolean)
compare?: (a: T, b: T) => number
} = {}, } = {},
) { ) {
const result = new Map(fetched.map((item) => [item.id, item])) const result = new Map(fetched.map((item) => [item.id, item]))
@@ -180,7 +179,8 @@ function reconcileFetched<T extends { id: string }>(
if (!item) result.delete(id) if (!item) result.delete(id)
} }
for (const id of options.removed ?? emptyIDs) result.delete(id) for (const id of options.removed ?? emptyIDs) result.delete(id)
return [...result.values()].sort((a, b) => cmp(a.id, b.id)) const items = [...result.values()]
return options.compare ? items.sort(options.compare) : items
} }
type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> } type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
@@ -413,8 +413,7 @@ export function createServerSession(
if (!load) return if (!load) return
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata. // A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
const messages = data.message[sessionID] const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found) if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
load.retainedMessages.add(messageID)
const parts = load.touchedParts.get(messageID) const parts = load.touchedParts.get(messageID)
if (parts) { if (parts) {
parts.add(partID) parts.add(partID)
@@ -437,16 +436,14 @@ export function createServerSession(
load.touchedParts.set(messageID, new Set(parts)) load.touchedParts.set(messageID, new Set(parts))
load.carriedDeltaParts.set(messageID, new Set(parts)) load.carriedDeltaParts.set(messageID, new Set(parts))
const messages = data.message[sessionID] const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found) if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
load.retainedMessages.add(messageID)
} }
for (const [messageID, parts] of load.removedParts) { for (const [messageID, parts] of load.removedParts) {
const touched = load.touchedParts.get(messageID) ?? new Set<string>() const touched = load.touchedParts.get(messageID) ?? new Set<string>()
parts.forEach((partID) => touched.add(partID)) parts.forEach((partID) => touched.add(partID))
load.touchedParts.set(messageID, touched) load.touchedParts.set(messageID, touched)
const messages = data.message[sessionID] const messages = data.message[sessionID]
if (messages && Binary.search(messages, messageID, (message) => message.id).found) if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
load.retainedMessages.add(messageID)
} }
for (const [messageID, parts] of load.optimisticParts) { for (const [messageID, parts] of load.optimisticParts) {
load.removedMessages.delete(messageID) load.removedMessages.delete(messageID)
@@ -555,7 +552,7 @@ export function createServerSession(
const source = pages.flatMap((page) => page.data).toReversed() const source = pages.flatMap((page) => page.data).toReversed()
const normalized = normalizeSessionMessages(sessionID, source) const normalized = normalizeSessionMessages(sessionID, source)
return { return {
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)), session: normalized.messages.sort(compareMessages),
part: [...normalized.parts.entries()] part: [...normalized.parts.entries()]
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) })) .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
.sort((a, b) => cmp(a.id, b.id)), .sort((a, b) => cmp(a.id, b.id)),
@@ -572,7 +569,7 @@ export function createServerSession(
}) })
const items = (response.data ?? []).filter((item) => !!item?.info?.id) const items = (response.data ?? []).filter((item) => !!item?.info?.id)
return { return {
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)), session: items.map((item) => cleanMessage(item.info)).sort(compareMessages),
part: items.map((item) => ({ part: items.map((item) => ({
id: item.info.id, id: item.info.id,
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)), part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
@@ -696,7 +693,7 @@ export function createServerSession(
const normalized = normalizeSessionMessages(sessionID, source) const normalized = normalizeSessionMessages(sessionID, source)
return { return {
...page, ...page,
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)), session: normalized.messages.sort(compareMessages),
part: [...normalized.parts.entries()] part: [...normalized.parts.entries()]
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) })) .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
.sort((a, b) => cmp(a.id, b.id)), .sort((a, b) => cmp(a.id, b.id)),
@@ -713,6 +710,7 @@ export function createServerSession(
retained: load?.retainedMessages, retained: load?.retainedMessages,
removed: load?.removedMessages, removed: load?.removedMessages,
preserveUnfetched, preserveUnfetched,
compare: compareMessages,
}) })
batch(() => { batch(() => {
if (source) setData("session_message", sessionID, reconcile(source)) if (source) setData("session_message", sessionID, reconcile(source))
@@ -754,7 +752,7 @@ export function createServerSession(
try { try {
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load)) const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
const first = page.session.reduce<Message | undefined>( const first = page.session.reduce<Message | undefined>(
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest), (oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
undefined, undefined,
) )
if (generations.get(sessionID) !== active) return if (generations.get(sessionID) !== active) return
@@ -804,14 +802,15 @@ export function createServerSession(
session: merge( session: merge(
page.session, page.session,
parents.map((parent) => parent.message), parents.map((parent) => parent.message),
), ).sort(compareMessages),
part: merge( part: merge(
page.part, page.part,
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })), parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
), ),
} }
const preserveUnfetched = const preserveUnfetched =
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0))) mode === "prepend" ||
(!result.complete && (!first || ((message: Message) => compareMessages(message, first) < 0)))
applyMessagePage( applyMessagePage(
sessionID, sessionID,
result, result,
@@ -928,7 +927,7 @@ export function createServerSession(
.message({ sessionID, messageID }) .message({ sessionID, messageID })
.then((message) => { .then((message) => {
const current = data.session_message[sessionID] ?? [] const current = data.session_message[sessionID] ?? []
const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id)) const messages = [...current.filter((item) => item.id !== message.id), message].sort(compareMessages)
projectV2({ sessionID, messages, touched: [message.id] }) projectV2({ sessionID, messages, touched: [message.id] })
}) })
.catch(() => {}) .catch(() => {})
@@ -1051,7 +1050,7 @@ export function createServerSession(
setData("message", info.sessionID, [info]) setData("message", info.sessionID, [info])
return return
} }
const result = Binary.search(messages, info.id, (message) => message.id) const result = Binary.search(messages, messageKey(info), messageKey)
if (result.found) setData("message", info.sessionID, result.index, reconcile(info)) if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
if (!result.found) if (!result.found)
setData("message", info.sessionID, (value = []) => { setData("message", info.sessionID, (value = []) => {
@@ -1084,8 +1083,8 @@ export function createServerSession(
produce((draft) => { produce((draft) => {
const messages = draft.message[props.sessionID] const messages = draft.message[props.sessionID]
if (messages) { if (messages) {
const result = Binary.search(messages, props.messageID, (message) => message.id) const index = messages.findIndex((message) => message.id === props.messageID)
if (result.found) messages.splice(result.index, 1) if (index >= 0) messages.splice(index, 1)
} }
deleteMessageParts(draft, props.messageID) deleteMessageParts(draft, props.messageID)
}), }),
@@ -1097,7 +1096,7 @@ export function createServerSession(
if (SKIP_PARTS.has(part.type)) return if (SKIP_PARTS.has(part.type)) return
const messages = data.message[part.sessionID] const messages = data.message[part.sessionID]
const load = messageLoads.get(part.sessionID) const load = messageLoads.get(part.sessionID)
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found const missing = !messages?.some((message) => message.id === part.messageID)
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan. // Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
if ( if (
missing && missing &&
@@ -1341,7 +1340,7 @@ export function createServerSession(
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] }) if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
if (!items) if (!items)
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]])) optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message])) setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]).sort(compareMessages))
setData( setData(
"part_text_accum_delta", "part_text_accum_delta",
produce((draft) => { produce((draft) => {
@@ -4,11 +4,11 @@ import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "
type Text = Extract<Part, { type: "text" }> type Text = Extract<Part, { type: "text" }>
const userMessage = (id: string, sessionID: string): Message => ({ const userMessage = (id: string, sessionID: string, created = 1): Message => ({
id, id,
sessionID, sessionID,
role: "user", role: "user",
time: { created: 1 }, time: { created },
agent: "assistant", agent: "assistant",
model: { providerID: "openai", modelID: "gpt" }, model: { providerID: "openai", modelID: "gpt" },
}) })
@@ -22,21 +22,21 @@ const textPart = (id: string, sessionID: string, messageID: string): Text => ({
}) })
describe("sync optimistic reducers", () => { describe("sync optimistic reducers", () => {
test("applyOptimisticAdd inserts message in sorted order and stores parts", () => { test("applyOptimisticAdd inserts by creation time", () => {
const sessionID = "ses_1" const sessionID = "ses_1"
const draft = { const draft = {
message: { [sessionID]: [userMessage("msg_2", sessionID)] }, message: { [sessionID]: [userMessage("msg_z", sessionID, 1)] },
part: {} as Record<string, Part[] | undefined>, part: {} as Record<string, Part[] | undefined>,
} }
applyOptimisticAdd(draft, { applyOptimisticAdd(draft, {
sessionID, sessionID,
message: userMessage("msg_1", sessionID), message: userMessage("msg_a", sessionID, 2),
parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")], parts: [textPart("prt_2", sessionID, "msg_a"), textPart("prt_1", sessionID, "msg_a")],
}) })
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"]) expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"]) expect(draft.part.msg_a?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
}) })
test("applyOptimisticRemove removes message and part entries", () => { test("applyOptimisticRemove removes message and part entries", () => {
@@ -60,19 +60,33 @@ describe("sync optimistic reducers", () => {
const sessionID = "ses_1" const sessionID = "ses_1"
const page = mergeOptimisticPage( const page = mergeOptimisticPage(
{ {
session: [userMessage("msg_1", sessionID)], session: [userMessage("msg_z", sessionID, 1)],
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }], part: [{ id: "msg_z", part: [textPart("prt_1", sessionID, "msg_z")] }],
complete: true, complete: true,
}, },
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }], [{ message: userMessage("msg_a", sessionID, 2), parts: [textPart("prt_2", sessionID, "msg_a")] }],
) )
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"]) expect(page.session.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"]) expect(page.part.find((x) => x.id === "msg_a")?.part.map((x) => x.id)).toEqual(["prt_2"])
expect(page.confirmed).toEqual([]) expect(page.confirmed).toEqual([])
expect(page.complete).toBe(true) expect(page.complete).toBe(true)
}) })
test("mergeOptimisticPage uses IDs only to break equal-time ties", () => {
const sessionID = "ses_1"
const page = mergeOptimisticPage(
{
session: [userMessage("msg_z", sessionID, 1)],
part: [],
complete: true,
},
[{ message: userMessage("msg_a", sessionID, 1), parts: [] }],
)
expect(page.session.map((message) => message.id)).toEqual(["msg_a", "msg_z"])
})
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => { test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
const sessionID = "ses_1" const sessionID = "ses_1"
const page = mergeOptimisticPage( const page = mergeOptimisticPage(
+5 -4
View File
@@ -3,6 +3,7 @@ import { createMemo } from "solid-js"
import { useServerSync } from "./server-sync" import { useServerSync } from "./server-sync"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { messageKey } from "@/utils/session-message"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@@ -67,7 +68,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
const confirmed: string[] = [] const confirmed: string[] = []
for (const item of items) { for (const item of items) {
const result = Binary.search(session, item.message.id, (message) => message.id) const result = Binary.search(session, messageKey(item.message), messageKey)
const found = result.found const found = result.found
if (!found) session.splice(result.index, 0, item.message) if (!found) session.splice(result.index, 0, item.message)
@@ -92,7 +93,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) { export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
const messages = draft.message[input.sessionID] const messages = draft.message[input.sessionID]
if (messages) { if (messages) {
const result = Binary.search(messages, input.message.id, (m) => m.id) const result = Binary.search(messages, messageKey(input.message), messageKey)
messages.splice(result.index, 0, input.message) messages.splice(result.index, 0, input.message)
} else { } else {
draft.message[input.sessionID] = [input.message] draft.message[input.sessionID] = [input.message]
@@ -103,8 +104,8 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) { export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
const messages = draft.message[input.sessionID] const messages = draft.message[input.sessionID]
if (messages) { if (messages) {
const result = Binary.search(messages, input.messageID, (m) => m.id) const index = messages.findIndex((message) => message.id === input.messageID)
if (result.found) messages.splice(result.index, 1) if (index >= 0) messages.splice(index, 1)
} }
delete draft.part[input.messageID] delete draft.part[input.messageID]
} }
+28 -25
View File
@@ -3,6 +3,7 @@
import * as Sentry from "@sentry/solid" import * as Sentry from "@sentry/solid"
import { render } from "solid-js/web" import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface } from "@/app" import { AppBaseProviders, AppInterface } from "@/app"
import { loadInitialLocale } from "@/context/language"
import { type Platform, PlatformProvider } from "@/context/platform" import { type Platform, PlatformProvider } from "@/context/platform"
import { createBrowserDraftStore } from "@/utils/draft-store" import { createBrowserDraftStore } from "@/utils/draft-store"
import { dict as en } from "@/i18n/en" import { dict as en } from "@/i18n/en"
@@ -149,29 +150,31 @@ if (import.meta.env.VITE_SENTRY_DSN) {
} }
if (root instanceof HTMLElement) { if (root instanceof HTMLElement) {
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token")) void loadInitialLocale().then((locale) => {
clearAuthToken() const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
const server: ServerConnection.Http = { clearAuthToken()
type: "http", const server: ServerConnection.Http = {
authToken: !!auth, type: "http",
http: { authToken: !!auth,
url: getCurrentUrl(), http: {
...auth, url: getCurrentUrl(),
}, ...auth,
} },
render( }
() => ( render(
<PlatformProvider value={platform}> () => (
<AppBaseProviders> <PlatformProvider value={platform}>
<AppInterface <AppBaseProviders locale={locale}>
defaultServer={ServerConnection.Key.make(getDefaultUrl())} <AppInterface
canonicalLocalServer={ServerConnection.key(server)} defaultServer={ServerConnection.Key.make(getDefaultUrl())}
servers={[server]} canonicalLocalServer={ServerConnection.key(server)}
disableHealthCheck servers={[server]}
/> disableHealthCheck
</AppBaseProviders> />
</PlatformProvider> </AppBaseProviders>
), </PlatformProvider>
root, ),
) root,
)
})
} }
File diff suppressed because it is too large Load Diff
+13
View File
@@ -184,6 +184,9 @@ export const dict = {
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة", "command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
"command.session.unshare": "إلغاء مشاركة الجلسة", "command.session.unshare": "إلغاء مشاركة الجلسة",
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة", "command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
"command.session.export": "تصدير الجلسة",
"command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON",
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات", "palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
"palette.search.placeholder.home": "البحث في الأوامر والجلسات", "palette.search.placeholder.home": "البحث في الأوامر والجلسات",
"palette.empty": "لا توجد نتائج", "palette.empty": "لا توجد نتائج",
@@ -531,6 +534,8 @@ export const dict = {
"dialog.project.edit.worktree.startup": "برنامج نصي لبدء تشغيل مساحة العمل", "dialog.project.edit.worktree.startup": "برنامج نصي لبدء تشغيل مساحة العمل",
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).", "dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install", "dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
"dialog.usageExceeded.dontShowAgain": "عدم الإظهار مرة أخرى",
"context.breakdown.title": "تفصيل السياق", "context.breakdown.title": "تفصيل السياق",
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال المميزة. يشمل "أخرى" تعريفات الأدوات والأعباء الإضافية.', "context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال المميزة. يشمل "أخرى" تعريفات الأدوات والأعباء الإضافية.',
"context.breakdown.system": "النظام", "context.breakdown.system": "النظام",
@@ -540,6 +545,8 @@ export const dict = {
"context.breakdown.other": "أخرى", "context.breakdown.other": "أخرى",
"context.systemPrompt.title": "موجّه النظام", "context.systemPrompt.title": "موجّه النظام",
"context.rawMessages.title": "الرسائل الخام", "context.rawMessages.title": "الرسائل الخام",
"context.export.session": "تصدير الجلسة",
"context.stats.session": "جلسة", "context.stats.session": "جلسة",
"context.stats.messages": "رسائل", "context.stats.messages": "رسائل",
"context.stats.provider": "موفر", "context.stats.provider": "موفر",
@@ -606,6 +613,11 @@ export const dict = {
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!", "toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة", "toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة", "toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
"toast.session.export.success.title": "تم تصدير الجلسة",
"toast.session.export.success.description": "تم حفظ الجلسة في \u2068{{filename}}\u2069",
"toast.session.export.failed.title": "فشل تصدير الجلسة",
"toast.session.export.failed.description": "حدث خطأ أثناء تصدير الجلسة",
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}", "toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
"toast.update.title": "تحديث متاح", "toast.update.title": "تحديث متاح",
"toast.update.description": "يتوفر الآن إصدار جديد من OpenCode ({{version}}) للتثبيت.", "toast.update.description": "يتوفر الآن إصدار جديد من OpenCode ({{version}}) للتثبيت.",
@@ -793,6 +805,7 @@ export const dict = {
"common.moreOptions": "مزيد من الخيارات", "common.moreOptions": "مزيد من الخيارات",
"common.learnMore": "اطّلع على المزيد", "common.learnMore": "اطّلع على المزيد",
"common.rename": "إعادة تسمية", "common.rename": "إعادة تسمية",
"common.export": "تصدير",
"common.reset": "إعادة تعيين", "common.reset": "إعادة تعيين",
"common.archive": "أرشفة", "common.archive": "أرشفة",
"common.delete": "حذف", "common.delete": "حذف",
+13
View File
@@ -180,6 +180,9 @@ export const dict = {
"command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala", "command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala",
"command.session.unshare": "Sessiyanın paylaşımını dayandır", "command.session.unshare": "Sessiyanın paylaşımını dayandır",
"command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır", "command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır",
"command.session.export": "Sessiyanı ixrac et",
"command.session.export.description": "Sessiyanın tam transkriptini JSON formatında ixrac et",
"palette.search.placeholder": "Fayl, əmr və sessiya axtar", "palette.search.placeholder": "Fayl, əmr və sessiya axtar",
"palette.search.placeholder.home": "Əmr və sessiyaları axtar", "palette.search.placeholder.home": "Əmr və sessiyaları axtar",
"palette.empty": "Nəticə tapılmadı", "palette.empty": "Nəticə tapılmadı",
@@ -541,6 +544,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Növbəti", "dialog.releaseNotes.action.next": "Növbəti",
"dialog.releaseNotes.action.hideFuture": "Gələcəkdə göstərmə", "dialog.releaseNotes.action.hideFuture": "Gələcəkdə göstərmə",
"dialog.releaseNotes.media.alt": "Buraxılış önbaxışı", "dialog.releaseNotes.media.alt": "Buraxılış önbaxışı",
"dialog.usageExceeded.dontShowAgain": "Bir daha göstərmə",
"context.breakdown.title": "Kontekst bölgüsü", "context.breakdown.title": "Kontekst bölgüsü",
"context.breakdown.note": 'Giriş tokenlərinin təxmini bölgüsü. "Digər" alət təriflərini və əlavə yükü əhatə edir.', "context.breakdown.note": 'Giriş tokenlərinin təxmini bölgüsü. "Digər" alət təriflərini və əlavə yükü əhatə edir.',
"context.breakdown.system": "Sistem", "context.breakdown.system": "Sistem",
@@ -550,6 +555,8 @@ export const dict = {
"context.breakdown.other": "Digər", "context.breakdown.other": "Digər",
"context.systemPrompt.title": "Sistem promptu", "context.systemPrompt.title": "Sistem promptu",
"context.rawMessages.title": "Xam mesajlar", "context.rawMessages.title": "Xam mesajlar",
"context.export.session": "Sessiyanı ixrac et",
"context.stats.session": "Sessiya", "context.stats.session": "Sessiya",
"context.stats.messages": "Mesajlar", "context.stats.messages": "Mesajlar",
"context.stats.provider": "Provayder", "context.stats.provider": "Provayder",
@@ -616,6 +623,11 @@ export const dict = {
"toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!", "toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!",
"toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi", "toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi",
"toast.session.unshare.failed.description": "Sessiyanın paylaşımını dayandırarkən xəta baş verdi", "toast.session.unshare.failed.description": "Sessiyanın paylaşımını dayandırarkən xəta baş verdi",
"toast.session.export.success.title": "Sessiya ixrac edildi",
"toast.session.export.success.description": "Sessiya {{filename}} faylına saxlanıldı",
"toast.session.export.failed.title": "Sessiya ixrac edilə bilmədi",
"toast.session.export.failed.description": "Sessiyanı ixrac edərkən xəta baş verdi",
"toast.session.listFailed.title": "{{project}} üçün sessiyalar yüklənə bilmədi", "toast.session.listFailed.title": "{{project}} üçün sessiyalar yüklənə bilmədi",
"toast.project.reloadFailed.title": "{{project}} yenidən yüklənə bilmədi", "toast.project.reloadFailed.title": "{{project}} yenidən yüklənə bilmədi",
"toast.update.title": "Yeniləmə mövcuddur", "toast.update.title": "Yeniləmə mövcuddur",
@@ -832,6 +844,7 @@ export const dict = {
"common.moreOptions": "Daha çox seçim", "common.moreOptions": "Daha çox seçim",
"common.learnMore": "Ətraflı öyrən", "common.learnMore": "Ətraflı öyrən",
"common.rename": "Adını dəyiş", "common.rename": "Adını dəyiş",
"common.export": "İxrac et",
"common.reset": "Sıfırla", "common.reset": "Sıfırla",
"common.archive": "Arxivlə", "common.archive": "Arxivlə",
"common.delete": "Sil", "common.delete": "Sil",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13
View File
@@ -186,6 +186,9 @@ export const dict = {
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência", "command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
"command.session.unshare": "Parar de compartilhar sessão", "command.session.unshare": "Parar de compartilhar sessão",
"command.session.unshare.description": "Parar de compartilhar esta sessão", "command.session.unshare.description": "Parar de compartilhar esta sessão",
"command.session.export": "Exportar sessão",
"command.session.export.description": "Exportar a transcrição completa da sessão como JSON",
"palette.search.placeholder": "Buscar arquivos, comandos e sessões", "palette.search.placeholder": "Buscar arquivos, comandos e sessões",
"palette.search.placeholder.home": "Buscar comandos e sessões", "palette.search.placeholder.home": "Buscar comandos e sessões",
"palette.empty": "Nenhum resultado encontrado", "palette.empty": "Nenhum resultado encontrado",
@@ -534,6 +537,8 @@ export const dict = {
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho", "dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).", "dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "Ex.: bun install", "dialog.project.edit.worktree.startup.placeholder": "Ex.: bun install",
"dialog.usageExceeded.dontShowAgain": "Não mostrar novamente",
"context.breakdown.title": "Detalhamento do contexto", "context.breakdown.title": "Detalhamento do contexto",
"context.breakdown.note": "context.breakdown.note":
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e sobrecarga.', 'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e sobrecarga.',
@@ -544,6 +549,8 @@ export const dict = {
"context.breakdown.other": "Outros", "context.breakdown.other": "Outros",
"context.systemPrompt.title": "Prompt do sistema", "context.systemPrompt.title": "Prompt do sistema",
"context.rawMessages.title": "Mensagens brutas", "context.rawMessages.title": "Mensagens brutas",
"context.export.session": "Exportar sessão",
"context.stats.session": "Sessão", "context.stats.session": "Sessão",
"context.stats.messages": "Mensagens", "context.stats.messages": "Mensagens",
"context.stats.provider": "Provedor", "context.stats.provider": "Provedor",
@@ -610,6 +617,11 @@ export const dict = {
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!", "toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão", "toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão", "toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
"toast.session.export.success.title": "Sessão exportada",
"toast.session.export.success.description": "Sessão salva em {{filename}}",
"toast.session.export.failed.title": "Falha ao exportar sessão",
"toast.session.export.failed.description": "Ocorreu um erro ao exportar a sessão",
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}", "toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
"toast.update.title": "Atualização disponível", "toast.update.title": "Atualização disponível",
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.", "toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
@@ -796,6 +808,7 @@ export const dict = {
"common.moreOptions": "Mais opções", "common.moreOptions": "Mais opções",
"common.learnMore": "Saiba mais", "common.learnMore": "Saiba mais",
"common.rename": "Renomear", "common.rename": "Renomear",
"common.export": "Exportar",
"common.reset": "Redefinir", "common.reset": "Redefinir",
"common.archive": "Arquivar", "common.archive": "Arquivar",
"common.delete": "Excluir", "common.delete": "Excluir",
+13
View File
@@ -193,6 +193,9 @@ export const dict = {
"command.session.unshare": "Ukini dijeljenje sesije", "command.session.unshare": "Ukini dijeljenje sesije",
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije", "command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
"command.session.export": "Izvezi sesiju",
"command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu",
"palette.search.placeholder": "Pretraži datoteke, komande i sesije", "palette.search.placeholder": "Pretraži datoteke, komande i sesije",
"palette.search.placeholder.home": "Pretraži komande i sesije", "palette.search.placeholder.home": "Pretraži komande i sesije",
"palette.empty": "Nema rezultata", "palette.empty": "Nema rezultata",
@@ -565,6 +568,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).", "dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install", "dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
"dialog.usageExceeded.dontShowAgain": "Nemoj više prikazivati",
"context.breakdown.title": "Razlaganje konteksta", "context.breakdown.title": "Razlaganje konteksta",
"context.breakdown.note": "context.breakdown.note":
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatno opterećenje.', 'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatno opterećenje.',
@@ -577,6 +582,8 @@ export const dict = {
"context.systemPrompt.title": "Sistemski prompt", "context.systemPrompt.title": "Sistemski prompt",
"context.rawMessages.title": "Sirove poruke", "context.rawMessages.title": "Sirove poruke",
"context.export.session": "Izvezi sesiju",
"context.stats.session": "Sesija", "context.stats.session": "Sesija",
"context.stats.messages": "Poruke", "context.stats.messages": "Poruke",
"context.stats.provider": "Provajder", "context.stats.provider": "Provajder",
@@ -655,6 +662,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja", "toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja", "toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
"toast.session.export.success.title": "Sesija izvezena",
"toast.session.export.success.description": "Sesija je sačuvana kao {{filename}}",
"toast.session.export.failed.title": "Izvoz sesije nije uspio",
"toast.session.export.failed.description": "Došlo je do greške prilikom izvoza sesije",
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}", "toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
"toast.update.title": "Dostupno ažuriranje", "toast.update.title": "Dostupno ažuriranje",
@@ -856,6 +868,7 @@ export const dict = {
"common.moreOptions": "Više opcija", "common.moreOptions": "Više opcija",
"common.learnMore": "Saznaj više", "common.learnMore": "Saznaj više",
"common.rename": "Preimenuj", "common.rename": "Preimenuj",
"common.export": "Izvezi",
"common.reset": "Vrati na početno stanje", "common.reset": "Vrati na početno stanje",
"common.archive": "Arhiviraj", "common.archive": "Arhiviraj",
"common.delete": "Izbriši", "common.delete": "Izbriši",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13
View File
@@ -92,6 +92,9 @@ export const dict = {
"command.session.unshare": "Stop deling af session", "command.session.unshare": "Stop deling af session",
"command.session.unshare.description": "Stop med at dele denne session", "command.session.unshare.description": "Stop med at dele denne session",
"command.session.export": "Eksportér session",
"command.session.export.description": "Eksportér hele sessionsudskriften som JSON",
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner", "palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
"palette.search.placeholder.home": "Søg i kommandoer og sessioner", "palette.search.placeholder.home": "Søg i kommandoer og sessioner",
"palette.empty": "Ingen resultater fundet", "palette.empty": "Ingen resultater fundet",
@@ -446,6 +449,8 @@ export const dict = {
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde", "dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).", "dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
"dialog.usageExceeded.dontShowAgain": "Vis ikke igen",
"context.breakdown.title": "Kontekstfordeling", "context.breakdown.title": "Kontekstfordeling",
"context.breakdown.note": "context.breakdown.note":
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.', 'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
@@ -458,6 +463,8 @@ export const dict = {
"context.systemPrompt.title": "Systemprompt", "context.systemPrompt.title": "Systemprompt",
"context.rawMessages.title": "Rå beskeder", "context.rawMessages.title": "Rå beskeder",
"context.export.session": "Eksportér session",
"context.stats.session": "Session", "context.stats.session": "Session",
"context.stats.messages": "Beskeder", "context.stats.messages": "Beskeder",
"context.stats.provider": "Udbyder", "context.stats.provider": "Udbyder",
@@ -535,6 +542,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session", "toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session",
"toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling", "toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling",
"toast.session.export.success.title": "Session eksporteret",
"toast.session.export.success.description": "Sessionen blev gemt i {{filename}}",
"toast.session.export.failed.title": "Kunne ikke eksportere session",
"toast.session.export.failed.description": "Der opstod en fejl under eksport af sessionen",
"toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}", "toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}",
"toast.update.title": "Opdatering tilgængelig", "toast.update.title": "Opdatering tilgængelig",
@@ -733,6 +745,7 @@ export const dict = {
"common.moreOptions": "Flere muligheder", "common.moreOptions": "Flere muligheder",
"common.learnMore": "Lær mere", "common.learnMore": "Lær mere",
"common.rename": "Omdøb", "common.rename": "Omdøb",
"common.export": "Eksportér",
"common.reset": "Nulstil", "common.reset": "Nulstil",
"common.archive": "Arkivér", "common.archive": "Arkivér",
"common.delete": "Slet", "common.delete": "Slet",
+13
View File
@@ -89,6 +89,9 @@ export const dict = {
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren", "command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
"command.session.unshare": "Teilen der Sitzung aufheben", "command.session.unshare": "Teilen der Sitzung aufheben",
"command.session.unshare.description": "Teilen dieser Sitzung beenden", "command.session.unshare.description": "Teilen dieser Sitzung beenden",
"command.session.export": "Sitzung exportieren",
"command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren",
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen", "palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen", "palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
"palette.empty": "Keine Ergebnisse gefunden", "palette.empty": "Keine Ergebnisse gefunden",
@@ -425,6 +428,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "dialog.project.edit.worktree.startup.description":
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install", "dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
"dialog.usageExceeded.dontShowAgain": "Nicht mehr anzeigen",
"context.breakdown.title": "Kontext-Aufschlüsselung", "context.breakdown.title": "Kontext-Aufschlüsselung",
"context.breakdown.note": "context.breakdown.note":
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.', 'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
@@ -435,6 +440,8 @@ export const dict = {
"context.breakdown.other": "Andere", "context.breakdown.other": "Andere",
"context.systemPrompt.title": "System-Prompt", "context.systemPrompt.title": "System-Prompt",
"context.rawMessages.title": "Rohdaten der Nachrichten", "context.rawMessages.title": "Rohdaten der Nachrichten",
"context.export.session": "Sitzung exportieren",
"context.stats.session": "Sitzung", "context.stats.session": "Sitzung",
"context.stats.messages": "Nachrichten", "context.stats.messages": "Nachrichten",
"context.stats.provider": "Anbieter", "context.stats.provider": "Anbieter",
@@ -501,6 +508,11 @@ export const dict = {
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!", "toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen", "toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
"toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten", "toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten",
"toast.session.export.success.title": "Sitzung exportiert",
"toast.session.export.success.description": "Sitzung unter {{filename}} gespeichert",
"toast.session.export.failed.title": "Sitzung konnte nicht exportiert werden",
"toast.session.export.failed.description": "Beim Exportieren der Sitzung ist ein Fehler aufgetreten",
"toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden", "toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden",
"toast.update.title": "Update verfügbar", "toast.update.title": "Update verfügbar",
"toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.", "toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.",
@@ -686,6 +698,7 @@ export const dict = {
"common.moreOptions": "Weitere Optionen", "common.moreOptions": "Weitere Optionen",
"common.learnMore": "Mehr erfahren", "common.learnMore": "Mehr erfahren",
"common.rename": "Umbenennen", "common.rename": "Umbenennen",
"common.export": "Exportieren",
"common.reset": "Zurücksetzen", "common.reset": "Zurücksetzen",
"common.archive": "Archivieren", "common.archive": "Archivieren",
"common.delete": "Löschen", "common.delete": "Löschen",
@@ -3,12 +3,83 @@ import {
createDesktopNativeBundle, createDesktopNativeBundle,
DESKTOP_NATIVE_ENGLISH, DESKTOP_NATIVE_ENGLISH,
DESKTOP_NATIVE_KEYS, DESKTOP_NATIVE_KEYS,
DESKTOP_NATIVE_LABELS,
DESKTOP_NATIVE_LOCALES,
DESKTOP_NATIVE_LOCALE_TAGS,
detectDesktopNativeLocale,
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES, DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
formatDesktopNativeMessage, formatDesktopNativeMessage,
parseDesktopNativeBundle, parseDesktopNativeBundle,
} from "./desktop-native" } from "./desktop-native"
describe("desktop native translations", () => { describe("desktop native translations", () => {
test("uses native language names independent of the active locale", () => {
expect(DESKTOP_NATIVE_LOCALES.map((locale) => DESKTOP_NATIVE_LABELS[locale])).toEqual([
"English",
"简体中文",
"繁體中文",
"한국어",
"Deutsch",
"Español",
"Français",
"Dansk",
"日本語",
"Polski",
"Русский",
"Українська",
"Bosanski",
"العربية",
"Norsk",
"Português (Brasil)",
"ไทย",
"Türkçe",
"हिन्दी",
"Nederlands",
"Bahasa Indonesia",
"Tiếng Việt",
"Italiano",
"اردو",
"پنجابی",
"Azərbaycanca",
"Suomi",
"Svenska",
"አማርኛ",
"Български",
"বাংলা",
"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",
])
})
test("accepts the exact typed bundle", () => { test("accepts the exact typed bundle", () => {
const bundle = createDesktopNativeBundle("en", (key) => DESKTOP_NATIVE_ENGLISH[key]) const bundle = createDesktopNativeBundle("en", (key) => DESKTOP_NATIVE_ENGLISH[key])
expect(parseDesktopNativeBundle(bundle)).toEqual(bundle) expect(parseDesktopNativeBundle(bundle)).toEqual(bundle)
@@ -39,3 +110,42 @@ describe("desktop native translations", () => {
expect(formatDesktopNativeMessage("{{known}} {{unknown}}", { known: "yes" })).toBe("yes {{unknown}}") expect(formatDesktopNativeMessage("{{known}} {{unknown}}", { known: "yes" })).toBe("yes {{unknown}}")
}) })
}) })
describe("desktop native locale detection", () => {
test("follows preference order and skips invalid or unsupported tags", () => {
expect(detectDesktopNativeLocale(["not_a_locale", "fr-FR"])).toBe("fr")
expect(detectDesktopNativeLocale(["eo", "de-DE"])).toBe("de")
})
test("uses Unicode likely subtags for script-sensitive bundles", () => {
expect(detectDesktopNativeLocale(["zh-TW"])).toBe("zht")
expect(detectDesktopNativeLocale(["zh-SG"])).toBe("zh")
expect(detectDesktopNativeLocale(["pa-PK"])).toBe("pa")
expect(detectDesktopNativeLocale(["pa-IN", "fr"])).toBe("fr")
expect(detectDesktopNativeLocale(["az-Cyrl", "de"])).toBe("de")
expect(detectDesktopNativeLocale(["sr-Cyrl"])).toBe("sr")
expect(detectDesktopNativeLocale(["sr-Latn", "en"])).toBe("en")
expect(detectDesktopNativeLocale(["uz-Latn"])).toBe("uz")
})
test("recognizes Norwegian language tags", () => {
expect(detectDesktopNativeLocale(["no"])).toBe("no")
expect(detectDesktopNativeLocale(["nb-NO"])).toBe("no")
expect(detectDesktopNativeLocale(["nn-NO"])).toBe("no")
})
})
describe("desktop native ICU data", () => {
test("accepts every locale in standard Intl formatters", () => {
for (const locale of DESKTOP_NATIVE_LOCALES) {
const tag = DESKTOP_NATIVE_LOCALE_TAGS[locale]
expect(() => new Intl.Locale(tag), `${locale} locale`).not.toThrow()
expect(() => new Intl.NumberFormat(tag), `${locale} number`).not.toThrow()
expect(() => new Intl.DateTimeFormat(tag), `${locale} date`).not.toThrow()
expect(() => new Intl.PluralRules(tag), `${locale} plural`).not.toThrow()
expect(() => new Intl.ListFormat(tag), `${locale} list`).not.toThrow()
expect(() => new Intl.DisplayNames(tag, { type: "language" }), `${locale} names`).not.toThrow()
expect(() => new Intl.Segmenter(tag), `${locale} segmenter`).not.toThrow()
}
})
})
+190
View File
@@ -27,10 +27,200 @@ 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]
export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
en: "English",
zh: "简体中文",
zht: "繁體中文",
ko: "한국어",
de: "Deutsch",
es: "Español",
fr: "Français",
da: "Dansk",
ja: "日本語",
pl: "Polski",
ru: "Русский",
uk: "Українська",
bs: "Bosanski",
ar: "العربية",
no: "Norsk",
br: "Português (Brasil)",
th: "ไทย",
tr: "Türkçe",
hi: "हिन्दी",
nl: "Nederlands",
id: "Bahasa Indonesia",
vi: "Tiếng Việt",
it: "Italiano",
ur: "اردو",
pa: "پنجابی",
az: "Azərbaycanca",
fi: "Suomi",
sv: "Svenska",
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 = {
"desktop.menu.app": "OpenCode", "desktop.menu.app": "OpenCode",
"desktop.menu.file": "File", "desktop.menu.file": "File",
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
+10
View File
@@ -95,6 +95,8 @@ export const dict = {
"command.session.share.description": "Share this session and copy the URL to clipboard", "command.session.share.description": "Share this session and copy the URL to clipboard",
"command.session.unshare": "Unshare session", "command.session.unshare": "Unshare session",
"command.session.unshare.description": "Stop sharing this session", "command.session.unshare.description": "Stop sharing this session",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"palette.search.placeholder": "Search files, commands, and sessions", "palette.search.placeholder": "Search files, commands, and sessions",
"palette.search.placeholder.home": "Search commands and sessions", "palette.search.placeholder.home": "Search commands and sessions",
@@ -478,6 +480,7 @@ export const dict = {
"dialog.releaseNotes.action.next": "Next", "dialog.releaseNotes.action.next": "Next",
"dialog.releaseNotes.action.hideFuture": "Don't show these in the future", "dialog.releaseNotes.action.hideFuture": "Don't show these in the future",
"dialog.releaseNotes.media.alt": "Release preview", "dialog.releaseNotes.media.alt": "Release preview",
"dialog.usageExceeded.dontShowAgain": "Don't show again",
"context.breakdown.title": "Context Breakdown", "context.breakdown.title": "Context Breakdown",
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.', "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
@@ -489,6 +492,7 @@ export const dict = {
"context.systemPrompt.title": "System Prompt", "context.systemPrompt.title": "System Prompt",
"context.rawMessages.title": "Raw messages", "context.rawMessages.title": "Raw messages",
"context.export.session": "Export session",
"context.stats.session": "Session", "context.stats.session": "Session",
"context.stats.messages": "Messages", "context.stats.messages": "Messages",
@@ -568,6 +572,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Failed to unshare session", "toast.session.unshare.failed.title": "Failed to unshare session",
"toast.session.unshare.failed.description": "An error occurred while unsharing the session", "toast.session.unshare.failed.description": "An error occurred while unsharing the session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"toast.session.listFailed.title": "Failed to load sessions for {{project}}", "toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}", "toast.project.reloadFailed.title": "Failed to reload {{project}}",
@@ -802,6 +811,7 @@ export const dict = {
"common.moreOptions": "More options", "common.moreOptions": "More options",
"common.learnMore": "Learn more", "common.learnMore": "Learn more",
"common.rename": "Rename", "common.rename": "Rename",
"common.export": "Export",
"common.reset": "Reset", "common.reset": "Reset",
"common.archive": "Archive", "common.archive": "Archive",
"common.delete": "Delete", "common.delete": "Delete",
+13
View File
@@ -193,6 +193,9 @@ export const dict = {
"command.session.unshare": "Dejar de compartir sesión", "command.session.unshare": "Dejar de compartir sesión",
"command.session.unshare.description": "Dejar de compartir esta sesión", "command.session.unshare.description": "Dejar de compartir esta sesión",
"command.session.export": "Exportar sesión",
"command.session.export.description": "Exportar la transcripción completa de la sesión como JSON",
"palette.search.placeholder": "Buscar archivos, comandos y sesiones", "palette.search.placeholder": "Buscar archivos, comandos y sesiones",
"palette.search.placeholder.home": "Buscar comandos y sesiones", "palette.search.placeholder.home": "Buscar comandos y sesiones",
"palette.empty": "No se encontraron resultados", "palette.empty": "No se encontraron resultados",
@@ -567,6 +570,8 @@ export const dict = {
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
"dialog.usageExceeded.dontShowAgain": "No volver a mostrar",
"context.breakdown.title": "Desglose del contexto", "context.breakdown.title": "Desglose del contexto",
"context.breakdown.note": "context.breakdown.note":
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
@@ -579,6 +584,8 @@ export const dict = {
"context.systemPrompt.title": "Prompt del sistema", "context.systemPrompt.title": "Prompt del sistema",
"context.rawMessages.title": "Mensajes en bruto", "context.rawMessages.title": "Mensajes en bruto",
"context.export.session": "Exportar sesión",
"context.stats.session": "Sesión", "context.stats.session": "Sesión",
"context.stats.messages": "Mensajes", "context.stats.messages": "Mensajes",
"context.stats.provider": "Proveedor", "context.stats.provider": "Proveedor",
@@ -657,6 +664,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión", "toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión",
"toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión", "toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión",
"toast.session.export.success.title": "Sesión exportada",
"toast.session.export.success.description": "Sesión guardada en {{filename}}",
"toast.session.export.failed.title": "No se pudo exportar la sesión",
"toast.session.export.failed.description": "Se produjo un error al exportar la sesión",
"toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}", "toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}",
"toast.update.title": "Actualización disponible", "toast.update.title": "Actualización disponible",
@@ -861,6 +873,7 @@ export const dict = {
"common.moreOptions": "Más opciones", "common.moreOptions": "Más opciones",
"common.learnMore": "Más información", "common.learnMore": "Más información",
"common.rename": "Renombrar", "common.rename": "Renombrar",
"common.export": "Exportar",
"common.reset": "Restablecer", "common.reset": "Restablecer",
"common.archive": "Archivar", "common.archive": "Archivar",
"common.delete": "Eliminar", "common.delete": "Eliminar",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13
View File
@@ -85,6 +85,9 @@ export const dict = {
"command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle", "command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle",
"command.session.unshare": "Peru istunnon jakaminen", "command.session.unshare": "Peru istunnon jakaminen",
"command.session.unshare.description": "Lopeta tämän istunnon jakaminen", "command.session.unshare.description": "Lopeta tämän istunnon jakaminen",
"command.session.export": "Vie istunto",
"command.session.export.description": "Vie istunnon koko transkriptio JSON-muodossa",
"palette.search.placeholder": "Hae tiedostoja, komentoja ja istuntoja", "palette.search.placeholder": "Hae tiedostoja, komentoja ja istuntoja",
"palette.search.placeholder.home": "Hae komentoja ja istuntoja", "palette.search.placeholder.home": "Hae komentoja ja istuntoja",
"palette.empty": "Tuloksia ei löytynyt", "palette.empty": "Tuloksia ei löytynyt",
@@ -429,6 +432,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Seuraava", "dialog.releaseNotes.action.next": "Seuraava",
"dialog.releaseNotes.action.hideFuture": "Älä näytä näitä jatkossa", "dialog.releaseNotes.action.hideFuture": "Älä näytä näitä jatkossa",
"dialog.releaseNotes.media.alt": "Julkaisun esikatselu", "dialog.releaseNotes.media.alt": "Julkaisun esikatselu",
"dialog.usageExceeded.dontShowAgain": "Älä näytä uudelleen",
"context.breakdown.title": "Kontekstin erittely", "context.breakdown.title": "Kontekstin erittely",
"context.breakdown.note": "context.breakdown.note":
'Syötetokenien arvioitu jakautuminen. "Muut" sisältää työkalumäärittelyt ja muun oheissisällön.', 'Syötetokenien arvioitu jakautuminen. "Muut" sisältää työkalumäärittelyt ja muun oheissisällön.',
@@ -439,6 +444,8 @@ export const dict = {
"context.breakdown.other": "Muut", "context.breakdown.other": "Muut",
"context.systemPrompt.title": "Järjestelmäkehote", "context.systemPrompt.title": "Järjestelmäkehote",
"context.rawMessages.title": "Raakaviestit", "context.rawMessages.title": "Raakaviestit",
"context.export.session": "Vie istunto",
"context.stats.session": "Istunto", "context.stats.session": "Istunto",
"context.stats.messages": "Viestit", "context.stats.messages": "Viestit",
"context.stats.provider": "Palveluntarjoaja", "context.stats.provider": "Palveluntarjoaja",
@@ -505,6 +512,11 @@ export const dict = {
"toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!", "toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!",
"toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui", "toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui",
"toast.session.unshare.failed.description": "Virhe peruttaessa istunnon jakamista", "toast.session.unshare.failed.description": "Virhe peruttaessa istunnon jakamista",
"toast.session.export.success.title": "Istunto viety",
"toast.session.export.success.description": "Istunto tallennettu tiedostoon {{filename}}",
"toast.session.export.failed.title": "Istunnon vieminen epäonnistui",
"toast.session.export.failed.description": "Istuntoa vietäessä tapahtui virhe",
"toast.session.listFailed.title": "Projektin {{project}} istuntojen lataaminen epäonnistui", "toast.session.listFailed.title": "Projektin {{project}} istuntojen lataaminen epäonnistui",
"toast.project.reloadFailed.title": "Projektin {{project}} lataaminen uudelleen epäonnistui", "toast.project.reloadFailed.title": "Projektin {{project}} lataaminen uudelleen epäonnistui",
"toast.update.title": "Päivitys saatavilla", "toast.update.title": "Päivitys saatavilla",
@@ -722,6 +734,7 @@ export const dict = {
"common.moreOptions": "Lisää vaihtoehtoja", "common.moreOptions": "Lisää vaihtoehtoja",
"common.learnMore": "Lue lisää", "common.learnMore": "Lue lisää",
"common.rename": "Nimeä uudelleen", "common.rename": "Nimeä uudelleen",
"common.export": "Vie",
"common.reset": "Palauta", "common.reset": "Palauta",
"common.archive": "Arkistoi", "common.archive": "Arkistoi",
"common.delete": "Poista", "common.delete": "Poista",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -186,6 +186,9 @@ export const dict = {
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers", "command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
"command.session.unshare": "Ne plus partager la session", "command.session.unshare": "Ne plus partager la session",
"command.session.unshare.description": "Arrêter de partager cette session", "command.session.unshare.description": "Arrêter de partager cette session",
"command.session.export": "Exporter la session",
"command.session.export.description": "Exporter la transcription intégrale de la session au format JSON",
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions", "palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
"palette.search.placeholder.home": "Rechercher des commandes et des sessions", "palette.search.placeholder.home": "Rechercher des commandes et des sessions",
"palette.empty": "Aucun résultat trouvé", "palette.empty": "Aucun résultat trouvé",
@@ -540,6 +543,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "dialog.project.edit.worktree.startup.description":
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).", "S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install", "dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
"dialog.usageExceeded.dontShowAgain": "Ne plus afficher",
"context.breakdown.title": "Répartition du contexte", "context.breakdown.title": "Répartition du contexte",
"context.breakdown.note": "context.breakdown.note":
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les données annexes.", "Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les données annexes.",
@@ -550,6 +555,8 @@ export const dict = {
"context.breakdown.other": "Autre", "context.breakdown.other": "Autre",
"context.systemPrompt.title": "Invite système", "context.systemPrompt.title": "Invite système",
"context.rawMessages.title": "Messages bruts", "context.rawMessages.title": "Messages bruts",
"context.export.session": "Exporter la session",
"context.stats.session": "Session", "context.stats.session": "Session",
"context.stats.messages": "Messages", "context.stats.messages": "Messages",
"context.stats.provider": "Fournisseur", "context.stats.provider": "Fournisseur",
@@ -617,6 +624,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Échec de la désactivation du partage", "toast.session.unshare.failed.title": "Échec de la désactivation du partage",
"toast.session.unshare.failed.description": "toast.session.unshare.failed.description":
"Une erreur s'est produite lors de la désactivation du partage de la session", "Une erreur s'est produite lors de la désactivation du partage de la session",
"toast.session.export.success.title": "Session exportée",
"toast.session.export.success.description": "Session enregistrée dans {{filename}}",
"toast.session.export.failed.title": "Échec de lexportation de la session",
"toast.session.export.failed.description": "Une erreur sest produite lors de lexportation de la session",
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
"toast.update.title": "Mise à jour disponible", "toast.update.title": "Mise à jour disponible",
"toast.update.description": "toast.update.description":
@@ -799,6 +811,7 @@ export const dict = {
"common.moreOptions": "Plus d'options", "common.moreOptions": "Plus d'options",
"common.learnMore": "En savoir plus", "common.learnMore": "En savoir plus",
"common.rename": "Renommer", "common.rename": "Renommer",
"common.export": "Exporter",
"common.reset": "Réinitialiser", "common.reset": "Réinitialiser",
"common.archive": "Archiver", "common.archive": "Archiver",
"common.delete": "Supprimer", "common.delete": "Supprimer",
+13
View File
@@ -185,6 +185,9 @@ export const dict = {
"command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें", "command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें",
"command.session.unshare": "सेशन साझा करना बंद करें", "command.session.unshare": "सेशन साझा करना बंद करें",
"command.session.unshare.description": "इस सेशन को साझा करना बंद करें", "command.session.unshare.description": "इस सेशन को साझा करना बंद करें",
"command.session.export": "सेशन निर्यात करें",
"command.session.export.description": "सेशन की पूरी ट्रांसक्रिप्ट को JSON के रूप में निर्यात करें",
"palette.search.placeholder": "फ़ाइलें, कमांड और सेशन खोजें", "palette.search.placeholder": "फ़ाइलें, कमांड और सेशन खोजें",
"palette.search.placeholder.home": "कमांड और सेशन खोजें", "palette.search.placeholder.home": "कमांड और सेशन खोजें",
"palette.empty": "कोई परिणाम नहीं मिला", "palette.empty": "कोई परिणाम नहीं मिला",
@@ -543,6 +546,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "अगला", "dialog.releaseNotes.action.next": "अगला",
"dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ", "dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ",
"dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन", "dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन",
"dialog.usageExceeded.dontShowAgain": "फिर से न दिखाएँ",
"context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन", "context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन",
"context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।', "context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।',
"context.breakdown.system": "सिस्टम", "context.breakdown.system": "सिस्टम",
@@ -552,6 +557,8 @@ export const dict = {
"context.breakdown.other": "अन्य", "context.breakdown.other": "अन्य",
"context.systemPrompt.title": "सिस्टम प्रॉम्प्ट", "context.systemPrompt.title": "सिस्टम प्रॉम्प्ट",
"context.rawMessages.title": "मूल संदेश", "context.rawMessages.title": "मूल संदेश",
"context.export.session": "सेशन निर्यात करें",
"context.stats.session": "सेशन", "context.stats.session": "सेशन",
"context.stats.messages": "संदेश", "context.stats.messages": "संदेश",
"context.stats.provider": "प्रोवाइडर", "context.stats.provider": "प्रोवाइडर",
@@ -618,6 +625,11 @@ export const dict = {
"toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!", "toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!",
"toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल", "toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल",
"toast.session.unshare.failed.description": "सेशन को अनशेयर करते समय एक त्रुटि उत्पन्न हुई", "toast.session.unshare.failed.description": "सेशन को अनशेयर करते समय एक त्रुटि उत्पन्न हुई",
"toast.session.export.success.title": "सेशन निर्यात किया गया",
"toast.session.export.success.description": "सेशन को {{filename}} में सहेजा गया",
"toast.session.export.failed.title": "सेशन निर्यात करने में विफल",
"toast.session.export.failed.description": "सेशन निर्यात करते समय एक त्रुटि हुई",
"toast.session.listFailed.title": "{{project}} के लिए सेशन लोड करने में विफल", "toast.session.listFailed.title": "{{project}} के लिए सेशन लोड करने में विफल",
"toast.project.reloadFailed.title": "{{project}} को पुनः लोड करने में विफल", "toast.project.reloadFailed.title": "{{project}} को पुनः लोड करने में विफल",
"toast.update.title": "उपलब्ध अद्यतन", "toast.update.title": "उपलब्ध अद्यतन",
@@ -833,6 +845,7 @@ export const dict = {
"common.moreOptions": "अधिक विकल्प", "common.moreOptions": "अधिक विकल्प",
"common.learnMore": "और अधिक जानें", "common.learnMore": "और अधिक जानें",
"common.rename": "नाम बदलें", "common.rename": "नाम बदलें",
"common.export": "निर्यात करें",
"common.reset": "रीसेट करें", "common.reset": "रीसेट करें",
"common.archive": "संग्रहित करें", "common.archive": "संग्रहित करें",
"common.delete": "हटाएँ", "common.delete": "हटाएँ",
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
+13
View File
@@ -193,6 +193,9 @@ export const dict = {
"command.session.unshare": "Hentikan berbagi", "command.session.unshare": "Hentikan berbagi",
"command.session.unshare.description": "Hentikan berbagi sesi ini", "command.session.unshare.description": "Hentikan berbagi sesi ini",
"command.session.export": "Ekspor sesi",
"command.session.export.description": "Ekspor transkrip sesi lengkap sebagai JSON",
"palette.search.placeholder": "Cari berkas, perintah, dan sesi", "palette.search.placeholder": "Cari berkas, perintah, dan sesi",
"palette.search.placeholder.home": "Cari perintah dan sesi", "palette.search.placeholder.home": "Cari perintah dan sesi",
"palette.empty": "Hasil tidak ditemukan", "palette.empty": "Hasil tidak ditemukan",
@@ -576,6 +579,8 @@ export const dict = {
"dialog.releaseNotes.action.hideFuture": "Jangan tampilkan ini di masa depan", "dialog.releaseNotes.action.hideFuture": "Jangan tampilkan ini di masa depan",
"dialog.releaseNotes.media.alt": "Pratinjau rilis", "dialog.releaseNotes.media.alt": "Pratinjau rilis",
"dialog.usageExceeded.dontShowAgain": "Jangan tampilkan lagi",
"context.breakdown.title": "Rincian Konteks", "context.breakdown.title": "Rincian Konteks",
"context.breakdown.note": 'Perkiraan rincian token masukan. "Lainnya" mencakup definisi alat dan beban tambahan.', "context.breakdown.note": 'Perkiraan rincian token masukan. "Lainnya" mencakup definisi alat dan beban tambahan.',
"context.breakdown.system": "Sistem", "context.breakdown.system": "Sistem",
@@ -587,6 +592,8 @@ export const dict = {
"context.systemPrompt.title": "Prompt Sistem", "context.systemPrompt.title": "Prompt Sistem",
"context.rawMessages.title": "Pesan mentah", "context.rawMessages.title": "Pesan mentah",
"context.export.session": "Ekspor sesi",
"context.stats.session": "Sesi", "context.stats.session": "Sesi",
"context.stats.messages": "Pesan", "context.stats.messages": "Pesan",
"context.stats.provider": "Penyedia", "context.stats.provider": "Penyedia",
@@ -664,6 +671,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Gagal menghentikan berbagi sesi", "toast.session.unshare.failed.title": "Gagal menghentikan berbagi sesi",
"toast.session.unshare.failed.description": "Terjadi kesalahan saat menghentikan berbagi sesi", "toast.session.unshare.failed.description": "Terjadi kesalahan saat menghentikan berbagi sesi",
"toast.session.export.success.title": "Sesi diekspor",
"toast.session.export.success.description": "Sesi disimpan ke {{filename}}",
"toast.session.export.failed.title": "Gagal mengekspor sesi",
"toast.session.export.failed.description": "Terjadi kesalahan saat mengekspor sesi",
"toast.session.listFailed.title": "Gagal memuat sesi untuk {{project}}", "toast.session.listFailed.title": "Gagal memuat sesi untuk {{project}}",
"toast.project.reloadFailed.title": "Gagal memuat ulang {{project}}", "toast.project.reloadFailed.title": "Gagal memuat ulang {{project}}",
@@ -897,6 +909,7 @@ export const dict = {
"common.moreOptions": "Opsi lainnya", "common.moreOptions": "Opsi lainnya",
"common.learnMore": "Pelajari lebih lanjut", "common.learnMore": "Pelajari lebih lanjut",
"common.rename": "Ganti nama", "common.rename": "Ganti nama",
"common.export": "Ekspor",
"common.reset": "Atur ulang", "common.reset": "Atur ulang",
"common.archive": "Arsipkan", "common.archive": "Arsipkan",
"common.delete": "Hapus", "common.delete": "Hapus",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -86,6 +86,9 @@ export const dict = {
"command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti", "command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti",
"command.session.unshare": "Annulla condivisione sessione", "command.session.unshare": "Annulla condivisione sessione",
"command.session.unshare.description": "Interrompi la condivisione di questa sessione", "command.session.unshare.description": "Interrompi la condivisione di questa sessione",
"command.session.export": "Esporta sessione",
"command.session.export.description": "Esporta la trascrizione completa della sessione in formato JSON",
"palette.search.placeholder": "Cerca file, comandi e sessioni", "palette.search.placeholder": "Cerca file, comandi e sessioni",
"palette.search.placeholder.home": "Cerca comandi e sessioni", "palette.search.placeholder.home": "Cerca comandi e sessioni",
"palette.empty": "Nessun risultato trovato", "palette.empty": "Nessun risultato trovato",
@@ -447,6 +450,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Avanti", "dialog.releaseNotes.action.next": "Avanti",
"dialog.releaseNotes.action.hideFuture": "Non mostrarli in futuro", "dialog.releaseNotes.action.hideFuture": "Non mostrarli in futuro",
"dialog.releaseNotes.media.alt": "Anteprima delle novità", "dialog.releaseNotes.media.alt": "Anteprima delle novità",
"dialog.usageExceeded.dontShowAgain": "Non mostrare più",
"context.breakdown.title": "Ripartizione del contesto", "context.breakdown.title": "Ripartizione del contesto",
"context.breakdown.note": "context.breakdown.note":
'Ripartizione approssimativa dei token di input. "Altro" include le definizioni degli strumenti e i dati aggiuntivi.', 'Ripartizione approssimativa dei token di input. "Altro" include le definizioni degli strumenti e i dati aggiuntivi.',
@@ -457,6 +462,8 @@ export const dict = {
"context.breakdown.other": "Altro", "context.breakdown.other": "Altro",
"context.systemPrompt.title": "Prompt di sistema", "context.systemPrompt.title": "Prompt di sistema",
"context.rawMessages.title": "Messaggi non elaborati", "context.rawMessages.title": "Messaggi non elaborati",
"context.export.session": "Esporta sessione",
"context.stats.session": "Sessione", "context.stats.session": "Sessione",
"context.stats.messages": "Messaggi", "context.stats.messages": "Messaggi",
"context.stats.provider": "Provider", "context.stats.provider": "Provider",
@@ -524,6 +531,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione", "toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione",
"toast.session.unshare.failed.description": "toast.session.unshare.failed.description":
"Si è verificato un errore durante l'annullamento della condivisione della sessione", "Si è verificato un errore durante l'annullamento della condivisione della sessione",
"toast.session.export.success.title": "Sessione esportata",
"toast.session.export.success.description": "Sessione salvata in {{filename}}",
"toast.session.export.failed.title": "Impossibile esportare la sessione",
"toast.session.export.failed.description": "Si è verificato un errore durante lesportazione della sessione",
"toast.session.listFailed.title": "Impossibile caricare le sessioni per {{project}}", "toast.session.listFailed.title": "Impossibile caricare le sessioni per {{project}}",
"toast.project.reloadFailed.title": "Impossibile ricaricare {{project}}", "toast.project.reloadFailed.title": "Impossibile ricaricare {{project}}",
"toast.update.title": "Aggiornamento disponibile", "toast.update.title": "Aggiornamento disponibile",
@@ -743,6 +755,7 @@ export const dict = {
"common.moreOptions": "Altre opzioni", "common.moreOptions": "Altre opzioni",
"common.learnMore": "Saperne di più", "common.learnMore": "Saperne di più",
"common.rename": "Rinomina", "common.rename": "Rinomina",
"common.export": "Esporta",
"common.reset": "Ripristina", "common.reset": "Ripristina",
"common.archive": "Archivia", "common.archive": "Archivia",
"common.delete": "Elimina", "common.delete": "Elimina",
+13
View File
@@ -184,6 +184,9 @@ export const dict = {
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー", "command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
"command.session.unshare": "セッションの共有を停止", "command.session.unshare": "セッションの共有を停止",
"command.session.unshare.description": "このセッションの共有を停止", "command.session.unshare.description": "このセッションの共有を停止",
"command.session.export": "セッションをエクスポート",
"command.session.export.description": "セッションの全記録を JSON としてエクスポート",
"palette.search.placeholder": "ファイル、コマンド、セッションを検索", "palette.search.placeholder": "ファイル、コマンド、セッションを検索",
"palette.search.placeholder.home": "コマンドとセッションを検索", "palette.search.placeholder.home": "コマンドとセッションを検索",
"palette.empty": "結果が見つかりません", "palette.empty": "結果が見つかりません",
@@ -533,6 +536,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "dialog.project.edit.worktree.startup.description":
"新しいワークスペース (ワークツリー) を作成した後に実行されます。", "新しいワークスペース (ワークツリー) を作成した後に実行されます。",
"dialog.project.edit.worktree.startup.placeholder": "例: bun install", "dialog.project.edit.worktree.startup.placeholder": "例: bun install",
"dialog.usageExceeded.dontShowAgain": "今後表示しない",
"context.breakdown.title": "コンテキストの内訳", "context.breakdown.title": "コンテキストの内訳",
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
"context.breakdown.system": "システム", "context.breakdown.system": "システム",
@@ -542,6 +547,8 @@ export const dict = {
"context.breakdown.other": "その他", "context.breakdown.other": "その他",
"context.systemPrompt.title": "システムプロンプト", "context.systemPrompt.title": "システムプロンプト",
"context.rawMessages.title": "生のメッセージ", "context.rawMessages.title": "生のメッセージ",
"context.export.session": "セッションをエクスポート",
"context.stats.session": "セッション", "context.stats.session": "セッション",
"context.stats.messages": "メッセージ", "context.stats.messages": "メッセージ",
"context.stats.provider": "プロバイダー", "context.stats.provider": "プロバイダー",
@@ -608,6 +615,11 @@ export const dict = {
"toast.session.unshare.success.description": "セッションの共有解除に成功しました!", "toast.session.unshare.success.description": "セッションの共有解除に成功しました!",
"toast.session.unshare.failed.title": "セッションの共有解除に失敗しました", "toast.session.unshare.failed.title": "セッションの共有解除に失敗しました",
"toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました", "toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました",
"toast.session.export.success.title": "セッションをエクスポートしました",
"toast.session.export.success.description": "セッションを {{filename}} に保存しました",
"toast.session.export.failed.title": "セッションのエクスポートに失敗しました",
"toast.session.export.failed.description": "セッションのエクスポート中にエラーが発生しました",
"toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました", "toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました",
"toast.update.title": "アップデートが利用可能です", "toast.update.title": "アップデートが利用可能です",
"toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。", "toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。",
@@ -781,6 +793,7 @@ export const dict = {
"common.moreOptions": "その他のオプション", "common.moreOptions": "その他のオプション",
"common.learnMore": "詳細", "common.learnMore": "詳細",
"common.rename": "名前を変更", "common.rename": "名前を変更",
"common.export": "エクスポート",
"common.reset": "リセット", "common.reset": "リセット",
"common.archive": "アーカイブ", "common.archive": "アーカイブ",
"common.delete": "削除", "common.delete": "削除",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13
View File
@@ -81,6 +81,9 @@ export const dict = {
"command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사", "command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사",
"command.session.unshare": "세션 공유 중지", "command.session.unshare": "세션 공유 중지",
"command.session.unshare.description": "이 세션 공유 중지", "command.session.unshare.description": "이 세션 공유 중지",
"command.session.export": "세션 내보내기",
"command.session.export.description": "전체 세션 기록을 JSON으로 내보내기",
"palette.search.placeholder": "파일, 명령어 및 세션 검색", "palette.search.placeholder": "파일, 명령어 및 세션 검색",
"palette.search.placeholder.home": "명령어 및 세션 검색", "palette.search.placeholder.home": "명령어 및 세션 검색",
"palette.empty": "결과 없음", "palette.empty": "결과 없음",
@@ -336,6 +339,8 @@ export const dict = {
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트", "dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.", "dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
"dialog.project.edit.worktree.startup.placeholder": "예: bun install", "dialog.project.edit.worktree.startup.placeholder": "예: bun install",
"dialog.usageExceeded.dontShowAgain": "다시 표시하지 않기",
"context.breakdown.title": "컨텍스트 분석", "context.breakdown.title": "컨텍스트 분석",
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.', "context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
"context.breakdown.system": "시스템", "context.breakdown.system": "시스템",
@@ -345,6 +350,8 @@ export const dict = {
"context.breakdown.other": "기타", "context.breakdown.other": "기타",
"context.systemPrompt.title": "시스템 프롬프트", "context.systemPrompt.title": "시스템 프롬프트",
"context.rawMessages.title": "원시 메시지", "context.rawMessages.title": "원시 메시지",
"context.export.session": "세션 내보내기",
"context.stats.session": "세션", "context.stats.session": "세션",
"context.stats.messages": "메시지", "context.stats.messages": "메시지",
"context.stats.provider": "공급자", "context.stats.provider": "공급자",
@@ -410,6 +417,11 @@ export const dict = {
"toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!", "toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!",
"toast.session.unshare.failed.title": "세션 공유 해제 실패", "toast.session.unshare.failed.title": "세션 공유 해제 실패",
"toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다", "toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다",
"toast.session.export.success.title": "세션을 내보냈습니다",
"toast.session.export.success.description": "세션 저장 위치: {{filename}}",
"toast.session.export.failed.title": "세션 내보내기 실패",
"toast.session.export.failed.description": "세션을 내보내는 동안 오류가 발생했습니다",
"toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다", "toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다",
"toast.update.title": "업데이트 가능", "toast.update.title": "업데이트 가능",
"toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.", "toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.",
@@ -544,6 +556,7 @@ export const dict = {
"common.moreOptions": "더 많은 옵션", "common.moreOptions": "더 많은 옵션",
"common.learnMore": "더 알아보기", "common.learnMore": "더 알아보기",
"common.rename": "이름 바꾸기", "common.rename": "이름 바꾸기",
"common.export": "내보내기",
"common.reset": "초기화", "common.reset": "초기화",
"common.archive": "보관", "common.archive": "보관",
"common.delete": "삭제", "common.delete": "삭제",
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
+13
View File
@@ -178,6 +178,9 @@ export const dict = {
"command.session.share.description": "Deel deze sessie en kopieer de URL naar het klembord", "command.session.share.description": "Deel deze sessie en kopieer de URL naar het klembord",
"command.session.unshare": "Sessie niet meer delen", "command.session.unshare": "Sessie niet meer delen",
"command.session.unshare.description": "Stop met het delen van deze sessie", "command.session.unshare.description": "Stop met het delen van deze sessie",
"command.session.export": "Sessie exporteren",
"command.session.export.description": "Het volledige sessietranscript exporteren als JSON",
"palette.search.placeholder": "Zoek naar bestanden, opdrachten en sessies", "palette.search.placeholder": "Zoek naar bestanden, opdrachten en sessies",
"palette.search.placeholder.home": "Opdrachten en sessies zoeken", "palette.search.placeholder.home": "Opdrachten en sessies zoeken",
"palette.empty": "Geen resultaten gevonden", "palette.empty": "Geen resultaten gevonden",
@@ -539,6 +542,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Volgende", "dialog.releaseNotes.action.next": "Volgende",
"dialog.releaseNotes.action.hideFuture": "Laat deze in de toekomst niet zien", "dialog.releaseNotes.action.hideFuture": "Laat deze in de toekomst niet zien",
"dialog.releaseNotes.media.alt": "Releasevoorbeeld", "dialog.releaseNotes.media.alt": "Releasevoorbeeld",
"dialog.usageExceeded.dontShowAgain": "Niet meer weergeven",
"context.breakdown.title": "Contextanalyse", "context.breakdown.title": "Contextanalyse",
"context.breakdown.note": "context.breakdown.note":
'Geschatte uitsplitsing van invoertokens. "Overig" omvat gereedschapsdefinities en overhead.', 'Geschatte uitsplitsing van invoertokens. "Overig" omvat gereedschapsdefinities en overhead.',
@@ -549,6 +554,8 @@ export const dict = {
"context.breakdown.other": "Overig", "context.breakdown.other": "Overig",
"context.systemPrompt.title": "Systeemprompt", "context.systemPrompt.title": "Systeemprompt",
"context.rawMessages.title": "Ruwe berichten", "context.rawMessages.title": "Ruwe berichten",
"context.export.session": "Sessie exporteren",
"context.stats.session": "Sessie", "context.stats.session": "Sessie",
"context.stats.messages": "Berichten", "context.stats.messages": "Berichten",
"context.stats.provider": "Aanbieder", "context.stats.provider": "Aanbieder",
@@ -616,6 +623,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Kan het delen van de sessie niet ongedaan maken", "toast.session.unshare.failed.title": "Kan het delen van de sessie niet ongedaan maken",
"toast.session.unshare.failed.description": "toast.session.unshare.failed.description":
"Er is een fout opgetreden bij het ongedaan maken van het delen van de sessie", "Er is een fout opgetreden bij het ongedaan maken van het delen van de sessie",
"toast.session.export.success.title": "Sessie geëxporteerd",
"toast.session.export.success.description": "Sessie opgeslagen in {{filename}}",
"toast.session.export.failed.title": "Kan sessie niet exporteren",
"toast.session.export.failed.description": "Er is een fout opgetreden tijdens het exporteren van de sessie",
"toast.session.listFailed.title": "Kan sessies voor {{project}} niet laden", "toast.session.listFailed.title": "Kan sessies voor {{project}} niet laden",
"toast.project.reloadFailed.title": "Kan {{project}} niet opnieuw laden", "toast.project.reloadFailed.title": "Kan {{project}} niet opnieuw laden",
"toast.update.title": "Update beschikbaar", "toast.update.title": "Update beschikbaar",
@@ -832,6 +844,7 @@ export const dict = {
"common.moreOptions": "Meer opties", "common.moreOptions": "Meer opties",
"common.learnMore": "Meer informatie", "common.learnMore": "Meer informatie",
"common.rename": "Hernoemen", "common.rename": "Hernoemen",
"common.export": "Exporteren",
"common.reset": "Opnieuw instellen", "common.reset": "Opnieuw instellen",
"common.archive": "Archiveren", "common.archive": "Archiveren",
"common.delete": "Verwijderen", "common.delete": "Verwijderen",
+13
View File
@@ -191,6 +191,9 @@ export const dict = {
"command.session.unshare": "Slutt å dele sesjon", "command.session.unshare": "Slutt å dele sesjon",
"command.session.unshare.description": "Slutt å dele denne sesjonen", "command.session.unshare.description": "Slutt å dele denne sesjonen",
"command.session.export": "Eksporter sesjon",
"command.session.export.description": "Eksporter hele sesjonsutskriften som JSON",
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner", "palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner", "palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
"palette.empty": "Ingen resultater funnet", "palette.empty": "Ingen resultater funnet",
@@ -470,6 +473,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.", "dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
"dialog.usageExceeded.dontShowAgain": "Ikke vis igjen",
"context.breakdown.title": "Kontekstfordeling", "context.breakdown.title": "Kontekstfordeling",
"context.breakdown.note": "context.breakdown.note":
'Omtrentlig fordeling av inndatatokener. "Annet" inkluderer verktøydefinisjoner og overhead.', 'Omtrentlig fordeling av inndatatokener. "Annet" inkluderer verktøydefinisjoner og overhead.',
@@ -482,6 +487,8 @@ export const dict = {
"context.systemPrompt.title": "Systemprompt", "context.systemPrompt.title": "Systemprompt",
"context.rawMessages.title": "Rå meldinger", "context.rawMessages.title": "Rå meldinger",
"context.export.session": "Eksporter sesjon",
"context.stats.session": "Sesjon", "context.stats.session": "Sesjon",
"context.stats.messages": "Meldinger", "context.stats.messages": "Meldinger",
"context.stats.provider": "Leverandør", "context.stats.provider": "Leverandør",
@@ -559,6 +566,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon", "toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes", "toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
"toast.session.export.success.title": "Sesjon eksportert",
"toast.session.export.success.description": "Sesjonen ble lagret i {{filename}}",
"toast.session.export.failed.title": "Kunne ikke eksportere sesjon",
"toast.session.export.failed.description": "Det oppstod en feil under eksport av sesjonen",
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}", "toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
"toast.update.title": "Oppdatering tilgjengelig", "toast.update.title": "Oppdatering tilgjengelig",
@@ -721,6 +733,7 @@ export const dict = {
"common.moreOptions": "Flere alternativer", "common.moreOptions": "Flere alternativer",
"common.learnMore": "Lær mer", "common.learnMore": "Lær mer",
"common.rename": "Gi nytt navn", "common.rename": "Gi nytt navn",
"common.export": "Eksporter",
"common.reset": "Tilbakestill", "common.reset": "Tilbakestill",
"common.archive": "Arkiver", "common.archive": "Arkiver",
"common.delete": "Slett", "common.delete": "Slett",
+13
View File
@@ -184,6 +184,9 @@ export const dict = {
"command.session.share.description": "اس سیشن نو شیئر کرو تے URL نو کلپ بورڈ تے کاپی کرو", "command.session.share.description": "اس سیشن نو شیئر کرو تے URL نو کلپ بورڈ تے کاپی کرو",
"command.session.unshare": "سیشن شیئر کرنا بند کرو", "command.session.unshare": "سیشن شیئر کرنا بند کرو",
"command.session.unshare.description": "اس سیشن نو شیئر کرنا بند کرو", "command.session.unshare.description": "اس سیشن نو شیئر کرنا بند کرو",
"command.session.export": "سیشن برآمد کرو",
"command.session.export.description": "سیشن دی پوری نقل JSON دی شکل وچ برآمد کرو",
"palette.search.placeholder": "فائلاں، کمانڈز، تے سیشنز تلاش کرو", "palette.search.placeholder": "فائلاں، کمانڈز، تے سیشنز تلاش کرو",
"palette.search.placeholder.home": "سرچ کمانڈز تے سیشنز", "palette.search.placeholder.home": "سرچ کمانڈز تے سیشنز",
"palette.empty": "کوئی نتیجہ نئیں ملیا", "palette.empty": "کوئی نتیجہ نئیں ملیا",
@@ -544,6 +547,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "اگلا", "dialog.releaseNotes.action.next": "اگلا",
"dialog.releaseNotes.action.hideFuture": "ایہہ اگوں نہ وکھاؤ", "dialog.releaseNotes.action.hideFuture": "ایہہ اگوں نہ وکھاؤ",
"dialog.releaseNotes.media.alt": "ریلیز پیش نظارہ", "dialog.releaseNotes.media.alt": "ریلیز پیش نظارہ",
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ وکھاؤ",
"context.breakdown.title": "کانٹیکسٹ دی ونڈ", "context.breakdown.title": "کانٹیکسٹ دی ونڈ",
"context.breakdown.note": 'ان پٹ ٹوکناں دی لگ بھگ ونڈ۔ "ہور" وچ ٹول دیاں تعریفاں تے وادھو خرچ شامل نیں۔', "context.breakdown.note": 'ان پٹ ٹوکناں دی لگ بھگ ونڈ۔ "ہور" وچ ٹول دیاں تعریفاں تے وادھو خرچ شامل نیں۔',
"context.breakdown.system": "نظام", "context.breakdown.system": "نظام",
@@ -553,6 +558,8 @@ export const dict = {
"context.breakdown.other": "ہور", "context.breakdown.other": "ہور",
"context.systemPrompt.title": "سسٹم پرامپٹ", "context.systemPrompt.title": "سسٹم پرامپٹ",
"context.rawMessages.title": "خام سنیہے", "context.rawMessages.title": "خام سنیہے",
"context.export.session": "سیشن برآمد کرو",
"context.stats.session": "سیشن", "context.stats.session": "سیشن",
"context.stats.messages": "سنیہے", "context.stats.messages": "سنیہے",
"context.stats.provider": "پرووائیڈر", "context.stats.provider": "پرووائیڈر",
@@ -619,6 +626,11 @@ export const dict = {
"toast.session.unshare.success.description": "سیشن دا شیئر کامیابی نال بند ہو گیا!", "toast.session.unshare.success.description": "سیشن دا شیئر کامیابی نال بند ہو گیا!",
"toast.session.unshare.failed.title": "سیشن ان شیئر کرن چ ناکام رہیا", "toast.session.unshare.failed.title": "سیشن ان شیئر کرن چ ناکام رہیا",
"toast.session.unshare.failed.description": "سیشن ان شیئر کردے ویلے کوئی غلطی ہو گئی", "toast.session.unshare.failed.description": "سیشن ان شیئر کردے ویلے کوئی غلطی ہو گئی",
"toast.session.export.success.title": "سیشن برآمد ہو گیا",
"toast.session.export.success.description": "سیشن نوں \u2068{{filename}}\u2069 وچ محفوظ کر دتا گیا",
"toast.session.export.failed.title": "سیشن برآمد کرن چ ناکامی ہوئی",
"toast.session.export.failed.description": "سیشن برآمد کردے ویلے اک غلطی ہو گئی",
"toast.session.listFailed.title": "{{project}} لئی سیشن لوڈ کرن چ ناکام رہیا", "toast.session.listFailed.title": "{{project}} لئی سیشن لوڈ کرن چ ناکام رہیا",
"toast.project.reloadFailed.title": "{{project}} دوبارہ لوڈ نئیں ہو سکیا", "toast.project.reloadFailed.title": "{{project}} دوبارہ لوڈ نئیں ہو سکیا",
"toast.update.title": "اپ ڈیٹ دستیاب اے", "toast.update.title": "اپ ڈیٹ دستیاب اے",
@@ -831,6 +843,7 @@ export const dict = {
"common.moreOptions": "ہور اختیارات", "common.moreOptions": "ہور اختیارات",
"common.learnMore": "ہور جانو", "common.learnMore": "ہور جانو",
"common.rename": "ناں بدلو", "common.rename": "ناں بدلو",
"common.export": "برآمد کرو",
"common.reset": "ری سیٹ کرو", "common.reset": "ری سیٹ کرو",
"common.archive": "آرکائیو کرو", "common.archive": "آرکائیو کرو",
"common.delete": "مکاؤ", "common.delete": "مکاؤ",
+49 -16
View File
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { desktopNativePluralCategories } from "./desktop-native"
const appLocales = [ const appLocales = [
"ar", "ar",
@@ -28,19 +29,51 @@ const appLocales = [
"az", "az",
"fi", "fi",
"sv", "sv",
"am",
"bg",
"bn",
"ca",
"cs",
"dv",
"dz",
"el",
"et",
"fa",
"fo",
"hr",
"hu",
"hy",
"is",
"ka",
"km",
"lo",
"lt",
"lv",
"mk",
"mn",
"ms",
"my",
"ne",
"ro",
"si",
"sk",
"sl",
"sq",
"sr",
"tg",
"tk",
"uz",
] as const ] as const
const desktopLocales = appLocales const desktopLocales = appLocales
const pluralCategories: Partial<Record<(typeof appLocales)[number], readonly string[]>> = { const pluralCategories = new Map(
ar: ["zero", "two", "few", "many"], appLocales.map(
br: ["many"], (locale) =>
bs: ["few"], [
es: ["many"], locale,
fr: ["many"], desktopNativePluralCategories(locale).filter((category) => category !== "one" && category !== "other"),
it: ["many"], ] as const,
pl: ["few", "many"], ),
ru: ["few", "many"], )
uk: ["few", "many"],
}
const domains = [ const domains = [
{ {
@@ -63,7 +96,7 @@ const domains = [
}, },
] as const ] as const
describe.skipIf(!!process.env.CI)("i18n parity", () => { describe("i18n parity", () => {
test("non-English locales have every English key and required plural variants", async () => { test("non-English locales have every English key and required plural variants", async () => {
for (const domain of domains) { for (const domain of domains) {
const source = await dictionary(domain.source) const source = await dictionary(domain.source)
@@ -74,7 +107,7 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
.filter((key) => !Object.hasOwn(source, key)) .filter((key) => !Object.hasOwn(source, key))
.sort() .sort()
const expected = pluralFamilies(source) const expected = pluralFamilies(source)
.flatMap((key) => (pluralCategories[locale] ?? []).map((category) => `${key}.${category}`)) .flatMap((key) => (pluralCategories.get(locale) ?? []).map((category) => `${key}.${category}`))
.sort() .sort()
expect({ domain: domain.name, locale, missing, extra }).toEqual({ expect({ domain: domain.name, locale, missing, extra }).toEqual({
domain: domain.name, domain: domain.name,
@@ -95,7 +128,7 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(), (key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
) )
const pluralMismatched = pluralFamilies(source).flatMap((key) => const pluralMismatched = pluralFamilies(source).flatMap((key) =>
(pluralCategories[locale] ?? []) (pluralCategories.get(locale) ?? [])
.map((category) => `${key}.${category}`) .map((category) => `${key}.${category}`)
.filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()), .filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()),
) )
@@ -144,12 +177,12 @@ describe("i18n plural parity", () => {
for (const locale of domain.locales) { for (const locale of domain.locales) {
const target = await dictionary(domain.target(locale)) const target = await dictionary(domain.target(locale))
const missing = families.flatMap((key) => const missing = families.flatMap((key) =>
(pluralCategories[locale] ?? []) (pluralCategories.get(locale) ?? [])
.map((category) => `${key}.${category}`) .map((category) => `${key}.${category}`)
.filter((variant) => !Object.hasOwn(target, variant)), .filter((variant) => !Object.hasOwn(target, variant)),
) )
const mismatched = families.flatMap((key) => const mismatched = families.flatMap((key) =>
(pluralCategories[locale] ?? []) (pluralCategories.get(locale) ?? [])
.map((category) => `${key}.${category}`) .map((category) => `${key}.${category}`)
.filter( .filter(
(variant) => (variant) =>
+13
View File
@@ -185,6 +185,9 @@ export const dict = {
"command.session.share.description": "Udostępnij tę sesję i skopiuj adres URL do schowka", "command.session.share.description": "Udostępnij tę sesję i skopiuj adres URL do schowka",
"command.session.unshare": "Przestań udostępniać sesję", "command.session.unshare": "Przestań udostępniać sesję",
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji", "command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
"command.session.export": "Eksportuj sesję",
"command.session.export.description": "Eksportuj pełny zapis sesji w formacie JSON",
"palette.search.placeholder": "Szukaj plików, poleceń i sesji", "palette.search.placeholder": "Szukaj plików, poleceń i sesji",
"palette.search.placeholder.home": "Szukaj poleceń i sesji", "palette.search.placeholder.home": "Szukaj poleceń i sesji",
"palette.empty": "Brak wyników", "palette.empty": "Brak wyników",
@@ -535,6 +538,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "dialog.project.edit.worktree.startup.description":
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (worktree).", "Uruchamiany po utworzeniu nowej przestrzeni roboczej (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "np. bun install", "dialog.project.edit.worktree.startup.placeholder": "np. bun install",
"dialog.usageExceeded.dontShowAgain": "Nie pokazuj ponownie",
"context.breakdown.title": "Podział kontekstu", "context.breakdown.title": "Podział kontekstu",
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
"context.breakdown.system": "System", "context.breakdown.system": "System",
@@ -544,6 +549,8 @@ export const dict = {
"context.breakdown.other": "Inne", "context.breakdown.other": "Inne",
"context.systemPrompt.title": "Prompt systemowy", "context.systemPrompt.title": "Prompt systemowy",
"context.rawMessages.title": "Surowe wiadomości", "context.rawMessages.title": "Surowe wiadomości",
"context.export.session": "Eksportuj sesję",
"context.stats.session": "Sesja", "context.stats.session": "Sesja",
"context.stats.messages": "Wiadomości", "context.stats.messages": "Wiadomości",
"context.stats.provider": "Dostawca", "context.stats.provider": "Dostawca",
@@ -610,6 +617,11 @@ export const dict = {
"toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!", "toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!",
"toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji", "toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji",
"toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji", "toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji",
"toast.session.export.success.title": "Sesja wyeksportowana",
"toast.session.export.success.description": "Sesję zapisano jako {{filename}}",
"toast.session.export.failed.title": "Nie udało się wyeksportować sesji",
"toast.session.export.failed.description": "Wystąpił błąd podczas eksportowania sesji",
"toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}", "toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}",
"toast.update.title": "Dostępna aktualizacja", "toast.update.title": "Dostępna aktualizacja",
"toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.", "toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.",
@@ -797,6 +809,7 @@ export const dict = {
"common.moreOptions": "Więcej opcji", "common.moreOptions": "Więcej opcji",
"common.learnMore": "Dowiedz się więcej", "common.learnMore": "Dowiedz się więcej",
"common.rename": "Zmień nazwę", "common.rename": "Zmień nazwę",
"common.export": "Eksportuj",
"common.reset": "Resetuj", "common.reset": "Resetuj",
"common.archive": "Archiwizuj", "common.archive": "Archiwizuj",
"common.delete": "Usuń", "common.delete": "Usuń",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -192,6 +192,9 @@ export const dict = {
"command.session.unshare": "Отменить публикацию", "command.session.unshare": "Отменить публикацию",
"command.session.unshare.description": "Прекратить публикацию сессии", "command.session.unshare.description": "Прекратить публикацию сессии",
"command.session.export": "Экспортировать сессию",
"command.session.export.description": "Экспортировать полную историю сессии в формате JSON",
"palette.search.placeholder": "Поиск файлов, команд и сессий", "palette.search.placeholder": "Поиск файлов, команд и сессий",
"palette.search.placeholder.home": "Поиск команд и сессий", "palette.search.placeholder.home": "Поиск команд и сессий",
"palette.empty": "Ничего не найдено", "palette.empty": "Ничего не найдено",
@@ -564,6 +567,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "dialog.project.edit.worktree.startup.description":
"Запускается после создания нового рабочего пространства (worktree).", "Запускается после создания нового рабочего пространства (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "например, bun install", "dialog.project.edit.worktree.startup.placeholder": "например, bun install",
"dialog.usageExceeded.dontShowAgain": "Больше не показывать",
"context.breakdown.title": "Разбивка контекста", "context.breakdown.title": "Разбивка контекста",
"context.breakdown.note": "context.breakdown.note":
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
@@ -576,6 +581,8 @@ export const dict = {
"context.systemPrompt.title": "Системный промпт", "context.systemPrompt.title": "Системный промпт",
"context.rawMessages.title": "Исходные сообщения", "context.rawMessages.title": "Исходные сообщения",
"context.export.session": "Экспортировать сессию",
"context.stats.session": "Сессия", "context.stats.session": "Сессия",
"context.stats.messages": "Сообщения", "context.stats.messages": "Сообщения",
"context.stats.provider": "Провайдер", "context.stats.provider": "Провайдер",
@@ -653,6 +660,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Не удалось отменить публикацию", "toast.session.unshare.failed.title": "Не удалось отменить публикацию",
"toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации", "toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации",
"toast.session.export.success.title": "Сессия экспортирована",
"toast.session.export.success.description": "Сессия сохранена в файл {{filename}}",
"toast.session.export.failed.title": "Не удалось экспортировать сессию",
"toast.session.export.failed.description": "Произошла ошибка при экспорте сессии",
"toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}", "toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}",
"toast.update.title": "Доступно обновление", "toast.update.title": "Доступно обновление",
@@ -858,6 +870,7 @@ export const dict = {
"common.moreOptions": "Дополнительные опции", "common.moreOptions": "Дополнительные опции",
"common.learnMore": "Подробнее", "common.learnMore": "Подробнее",
"common.rename": "Переименовать", "common.rename": "Переименовать",
"common.export": "Экспортировать",
"common.reset": "Сбросить", "common.reset": "Сбросить",
"common.archive": "Архивировать", "common.archive": "Архивировать",
"common.delete": "Удалить", "common.delete": "Удалить",
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
+13
View File
@@ -179,6 +179,9 @@ export const dict = {
"command.session.share.description": "Dela den här sessionen och kopiera URL:en till urklipp", "command.session.share.description": "Dela den här sessionen och kopiera URL:en till urklipp",
"command.session.unshare": "Sluta dela session", "command.session.unshare": "Sluta dela session",
"command.session.unshare.description": "Sluta dela den här sessionen", "command.session.unshare.description": "Sluta dela den här sessionen",
"command.session.export": "Exportera session",
"command.session.export.description": "Exportera hela sessionsutskriften som JSON",
"palette.search.placeholder": "Sök efter filer, kommandon och sessioner", "palette.search.placeholder": "Sök efter filer, kommandon och sessioner",
"palette.search.placeholder.home": "Sök bland kommandon och sessioner", "palette.search.placeholder.home": "Sök bland kommandon och sessioner",
"palette.empty": "Inga resultat hittades", "palette.empty": "Inga resultat hittades",
@@ -538,6 +541,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Nästa", "dialog.releaseNotes.action.next": "Nästa",
"dialog.releaseNotes.action.hideFuture": "Visa inte dessa i framtiden", "dialog.releaseNotes.action.hideFuture": "Visa inte dessa i framtiden",
"dialog.releaseNotes.media.alt": "Förhandsvisning av version", "dialog.releaseNotes.media.alt": "Förhandsvisning av version",
"dialog.usageExceeded.dontShowAgain": "Visa inte igen",
"context.breakdown.title": "Kontextfördelning", "context.breakdown.title": "Kontextfördelning",
"context.breakdown.note": "context.breakdown.note":
'Ungefärlig uppdelning av inmatningstokens. "Övrigt" inkluderar verktygsdefinitioner och overhead.', 'Ungefärlig uppdelning av inmatningstokens. "Övrigt" inkluderar verktygsdefinitioner och overhead.',
@@ -548,6 +553,8 @@ export const dict = {
"context.breakdown.other": "Övrigt", "context.breakdown.other": "Övrigt",
"context.systemPrompt.title": "Systemprompt", "context.systemPrompt.title": "Systemprompt",
"context.rawMessages.title": "Råmeddelanden", "context.rawMessages.title": "Råmeddelanden",
"context.export.session": "Exportera session",
"context.stats.session": "Session", "context.stats.session": "Session",
"context.stats.messages": "Meddelanden", "context.stats.messages": "Meddelanden",
"context.stats.provider": "Leverantör", "context.stats.provider": "Leverantör",
@@ -614,6 +621,11 @@ export const dict = {
"toast.session.unshare.success.description": "Delningen av sessionen har avslutats!", "toast.session.unshare.success.description": "Delningen av sessionen har avslutats!",
"toast.session.unshare.failed.title": "Det gick inte att avsluta delningen", "toast.session.unshare.failed.title": "Det gick inte att avsluta delningen",
"toast.session.unshare.failed.description": "Ett fel uppstod när delningen skulle avslutas", "toast.session.unshare.failed.description": "Ett fel uppstod när delningen skulle avslutas",
"toast.session.export.success.title": "Session exporterad",
"toast.session.export.success.description": "Sessionen sparades i {{filename}}",
"toast.session.export.failed.title": "Det gick inte att exportera sessionen",
"toast.session.export.failed.description": "Ett fel uppstod när sessionen exporterades",
"toast.session.listFailed.title": "Det gick inte att läsa in sessioner för {{project}}", "toast.session.listFailed.title": "Det gick inte att läsa in sessioner för {{project}}",
"toast.project.reloadFailed.title": "Det gick inte att ladda om {{project}}", "toast.project.reloadFailed.title": "Det gick inte att ladda om {{project}}",
"toast.update.title": "Uppdatering tillgänglig", "toast.update.title": "Uppdatering tillgänglig",
@@ -828,6 +840,7 @@ export const dict = {
"common.moreOptions": "Fler alternativ", "common.moreOptions": "Fler alternativ",
"common.learnMore": "Läs mer", "common.learnMore": "Läs mer",
"common.rename": "Byt namn", "common.rename": "Byt namn",
"common.export": "Exportera",
"common.reset": "Återställ", "common.reset": "Återställ",
"common.archive": "Arkivera", "common.archive": "Arkivera",
"common.delete": "Radera", "common.delete": "Radera",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -191,6 +191,9 @@ export const dict = {
"command.session.unshare": "ยกเลิกการแชร์เซสชัน", "command.session.unshare": "ยกเลิกการแชร์เซสชัน",
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้", "command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
"command.session.export": "ส่งออกเซสชัน",
"command.session.export.description": "ส่งออกบันทึกทั้งหมดของเซสชันเป็น JSON",
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน", "palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน", "palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
"palette.empty": "ไม่พบผลลัพธ์", "palette.empty": "ไม่พบผลลัพธ์",
@@ -561,6 +564,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)", "dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)",
"dialog.project.edit.worktree.startup.placeholder": "เช่น bun install", "dialog.project.edit.worktree.startup.placeholder": "เช่น bun install",
"dialog.usageExceeded.dontShowAgain": "ไม่ต้องแสดงอีก",
"context.breakdown.title": "การแบ่งบริบท", "context.breakdown.title": "การแบ่งบริบท",
"context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด', "context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด',
"context.breakdown.system": "ระบบ", "context.breakdown.system": "ระบบ",
@@ -572,6 +577,8 @@ export const dict = {
"context.systemPrompt.title": "พรอมต์ระบบ", "context.systemPrompt.title": "พรอมต์ระบบ",
"context.rawMessages.title": "ข้อความดิบ", "context.rawMessages.title": "ข้อความดิบ",
"context.export.session": "ส่งออกเซสชัน",
"context.stats.session": "เซสชัน", "context.stats.session": "เซสชัน",
"context.stats.messages": "ข้อความ", "context.stats.messages": "ข้อความ",
"context.stats.provider": "ผู้ให้บริการ", "context.stats.provider": "ผู้ให้บริการ",
@@ -650,6 +657,11 @@ export const dict = {
"toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน", "toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน",
"toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน", "toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน",
"toast.session.export.success.title": "ส่งออกเซสชันแล้ว",
"toast.session.export.success.description": "บันทึกเซสชันไปยัง {{filename}} แล้ว",
"toast.session.export.failed.title": "ไม่สามารถส่งออกเซสชัน",
"toast.session.export.failed.description": "เกิดข้อผิดพลาดขณะส่งออกเซสชัน",
"toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}", "toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}",
"toast.update.title": "มีการอัปเดต", "toast.update.title": "มีการอัปเดต",
@@ -843,6 +855,7 @@ export const dict = {
"common.moreOptions": "ตัวเลือกเพิ่มเติม", "common.moreOptions": "ตัวเลือกเพิ่มเติม",
"common.learnMore": "เรียนรู้เพิ่มเติม", "common.learnMore": "เรียนรู้เพิ่มเติม",
"common.rename": "เปลี่ยนชื่อ", "common.rename": "เปลี่ยนชื่อ",
"common.export": "ส่งออก",
"common.reset": "รีเซ็ต", "common.reset": "รีเซ็ต",
"common.archive": "จัดเก็บ", "common.archive": "จัดเก็บ",
"common.delete": "ลบ", "common.delete": "ลบ",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -197,6 +197,9 @@ export const dict = {
"command.session.unshare": "Paylaşımı kaldır", "command.session.unshare": "Paylaşımı kaldır",
"command.session.unshare.description": "Bu oturumun paylaşımını durdur", "command.session.unshare.description": "Bu oturumun paylaşımını durdur",
"command.session.export": "Oturumu dışa aktar",
"command.session.export.description": "Oturumun tam dökümünü JSON olarak dışa aktar",
"palette.search.placeholder": "Dosya, komut ve oturum ara", "palette.search.placeholder": "Dosya, komut ve oturum ara",
"palette.search.placeholder.home": "Komut ve oturum ara", "palette.search.placeholder.home": "Komut ve oturum ara",
"palette.empty": "Sonuç bulunamadı", "palette.empty": "Sonuç bulunamadı",
@@ -569,6 +572,8 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.", "dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.",
"dialog.project.edit.worktree.startup.placeholder": "örneğin bun install", "dialog.project.edit.worktree.startup.placeholder": "örneğin bun install",
"dialog.usageExceeded.dontShowAgain": "Bir daha gösterme",
"context.breakdown.title": "Bağlam Dökümü", "context.breakdown.title": "Bağlam Dökümü",
"context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.', "context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.',
"context.breakdown.system": "Sistem", "context.breakdown.system": "Sistem",
@@ -580,6 +585,8 @@ export const dict = {
"context.systemPrompt.title": "Sistem İstemi", "context.systemPrompt.title": "Sistem İstemi",
"context.rawMessages.title": "Ham mesajlar", "context.rawMessages.title": "Ham mesajlar",
"context.export.session": "Oturumu dışa aktar",
"context.stats.session": "Oturum", "context.stats.session": "Oturum",
"context.stats.messages": "Mesajlar", "context.stats.messages": "Mesajlar",
"context.stats.provider": "Sağlayıcı", "context.stats.provider": "Sağlayıcı",
@@ -658,6 +665,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı", "toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı",
"toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu", "toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu",
"toast.session.export.success.title": "Oturum dışa aktarıldı",
"toast.session.export.success.description": "Oturum {{filename}} dosyasına kaydedildi",
"toast.session.export.failed.title": "Oturum dışa aktarılamadı",
"toast.session.export.failed.description": "Oturum dışa aktarılırken bir hata oluştu",
"toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi", "toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi",
"toast.update.title": "Güncelleme mevcut", "toast.update.title": "Güncelleme mevcut",
@@ -861,6 +873,7 @@ export const dict = {
"common.moreOptions": "Daha fazla seçenek", "common.moreOptions": "Daha fazla seçenek",
"common.learnMore": "Daha fazla bilgi", "common.learnMore": "Daha fazla bilgi",
"common.rename": "Yeniden adlandır", "common.rename": "Yeniden adlandır",
"common.export": "Dışa aktar",
"common.reset": "Sıfırla", "common.reset": "Sıfırla",
"common.archive": "Arşivle", "common.archive": "Arşivle",
"common.delete": "Sil", "common.delete": "Sil",
+13
View File
@@ -193,6 +193,9 @@ export const dict = {
"command.session.unshare": "Припинити поширення сесії", "command.session.unshare": "Припинити поширення сесії",
"command.session.unshare.description": "Припинити поширення цієї сесії", "command.session.unshare.description": "Припинити поширення цієї сесії",
"command.session.export": "Експортувати сесію",
"command.session.export.description": "Експортувати повну історію сесії у форматі JSON",
"palette.search.placeholder": "Пошук файлів, команд і сесій", "palette.search.placeholder": "Пошук файлів, команд і сесій",
"palette.search.placeholder.home": "Пошук команд і сесій", "palette.search.placeholder.home": "Пошук команд і сесій",
"palette.empty": "Результатів не знайдено", "palette.empty": "Результатів не знайдено",
@@ -578,6 +581,8 @@ export const dict = {
"dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому", "dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому",
"dialog.releaseNotes.media.alt": "Попередній перегляд релізу", "dialog.releaseNotes.media.alt": "Попередній перегляд релізу",
"dialog.usageExceeded.dontShowAgain": "Більше не показувати",
"context.breakdown.title": "Розподіл контексту", "context.breakdown.title": "Розподіл контексту",
"context.breakdown.note": "context.breakdown.note":
'Приблизний розподіл вхідних токенів. "Інше" включає визначення інструментів і накладні витрати.', 'Приблизний розподіл вхідних токенів. "Інше" включає визначення інструментів і накладні витрати.',
@@ -590,6 +595,8 @@ export const dict = {
"context.systemPrompt.title": "Системний запит", "context.systemPrompt.title": "Системний запит",
"context.rawMessages.title": "Необроблені повідомлення", "context.rawMessages.title": "Необроблені повідомлення",
"context.export.session": "Експортувати сесію",
"context.stats.session": "Сесія", "context.stats.session": "Сесія",
"context.stats.messages": "Повідомлення", "context.stats.messages": "Повідомлення",
"context.stats.provider": "Провайдер", "context.stats.provider": "Провайдер",
@@ -668,6 +675,11 @@ export const dict = {
"toast.session.unshare.failed.title": "Не вдалося припинити поширення сесії", "toast.session.unshare.failed.title": "Не вдалося припинити поширення сесії",
"toast.session.unshare.failed.description": "Під час припинення поширення сесії сталася помилка", "toast.session.unshare.failed.description": "Під час припинення поширення сесії сталася помилка",
"toast.session.export.success.title": "Сесію експортовано",
"toast.session.export.success.description": "Сесію збережено у файл {{filename}}",
"toast.session.export.failed.title": "Не вдалося експортувати сесію",
"toast.session.export.failed.description": "Під час експортування сесії сталася помилка",
"toast.session.listFailed.title": "Не вдалося завантажити сесії для {{project}}", "toast.session.listFailed.title": "Не вдалося завантажити сесії для {{project}}",
"toast.project.reloadFailed.title": "Не вдалося перезавантажити {{project}}", "toast.project.reloadFailed.title": "Не вдалося перезавантажити {{project}}",
@@ -910,6 +922,7 @@ export const dict = {
"common.moreOptions": "Більше параметрів", "common.moreOptions": "Більше параметрів",
"common.learnMore": "Дізнатися більше", "common.learnMore": "Дізнатися більше",
"common.rename": "Перейменувати", "common.rename": "Перейменувати",
"common.export": "Експортувати",
"common.reset": "Скинути", "common.reset": "Скинути",
"common.archive": "Архівувати", "common.archive": "Архівувати",
"common.delete": "Видалити", "common.delete": "Видалити",
+13
View File
@@ -186,6 +186,9 @@ export const dict = {
"command.session.share.description": "اس سیشن کا اشتراک کریں اور URL کو کلپ بورڈ میں کاپی کریں۔", "command.session.share.description": "اس سیشن کا اشتراک کریں اور URL کو کلپ بورڈ میں کاپی کریں۔",
"command.session.unshare": "سیشن کا اشتراک ختم کریں۔", "command.session.unshare": "سیشن کا اشتراک ختم کریں۔",
"command.session.unshare.description": "اس سیشن کا اشتراک کرنا بند کریں۔", "command.session.unshare.description": "اس سیشن کا اشتراک کرنا بند کریں۔",
"command.session.export": "سیشن برآمد کریں",
"command.session.export.description": "سیشن کی مکمل نقل JSON کی صورت میں برآمد کریں",
"palette.search.placeholder": "فائلیں، کمانڈز اور سیشنز تلاش کریں۔", "palette.search.placeholder": "فائلیں، کمانڈز اور سیشنز تلاش کریں۔",
"palette.search.placeholder.home": "کمانڈز اور سیشن تلاش کریں۔", "palette.search.placeholder.home": "کمانڈز اور سیشن تلاش کریں۔",
"palette.empty": "کوئی نتیجہ نہیں ملا", "palette.empty": "کوئی نتیجہ نہیں ملا",
@@ -546,6 +549,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "اگلا", "dialog.releaseNotes.action.next": "اگلا",
"dialog.releaseNotes.action.hideFuture": "مستقبل میں یہ نہ دکھائیں۔", "dialog.releaseNotes.action.hideFuture": "مستقبل میں یہ نہ دکھائیں۔",
"dialog.releaseNotes.media.alt": "ریلیز کا پیش منظر", "dialog.releaseNotes.media.alt": "ریلیز کا پیش منظر",
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ دکھائیں",
"context.breakdown.title": "سیاق و سباق کی تفصیل", "context.breakdown.title": "سیاق و سباق کی تفصیل",
"context.breakdown.note": 'ان پٹ ٹوکنز کی تخمینی تقسیم۔ "دیگر" میں ٹول کی تعریفیں اور اضافی بوجھ شامل ہیں۔', "context.breakdown.note": 'ان پٹ ٹوکنز کی تخمینی تقسیم۔ "دیگر" میں ٹول کی تعریفیں اور اضافی بوجھ شامل ہیں۔',
"context.breakdown.system": "سسٹم", "context.breakdown.system": "سسٹم",
@@ -555,6 +560,8 @@ export const dict = {
"context.breakdown.other": "دیگر", "context.breakdown.other": "دیگر",
"context.systemPrompt.title": "سسٹم پرامپٹ", "context.systemPrompt.title": "سسٹم پرامپٹ",
"context.rawMessages.title": "خام پیغامات", "context.rawMessages.title": "خام پیغامات",
"context.export.session": "سیشن برآمد کریں",
"context.stats.session": "سیشن", "context.stats.session": "سیشن",
"context.stats.messages": "پیغامات", "context.stats.messages": "پیغامات",
"context.stats.provider": "فراہم کنندہ", "context.stats.provider": "فراہم کنندہ",
@@ -621,6 +628,11 @@ export const dict = {
"toast.session.unshare.success.description": "سیشن کا اشتراک کامیابی سے ختم ہو گیا!", "toast.session.unshare.success.description": "سیشن کا اشتراک کامیابی سے ختم ہو گیا!",
"toast.session.unshare.failed.title": "سیشن کا اشتراک ختم کرنے میں ناکام", "toast.session.unshare.failed.title": "سیشن کا اشتراک ختم کرنے میں ناکام",
"toast.session.unshare.failed.description": "سیشن کا اشتراک ختم کرتے وقت ایک خرابی پیش آگئی", "toast.session.unshare.failed.description": "سیشن کا اشتراک ختم کرتے وقت ایک خرابی پیش آگئی",
"toast.session.export.success.title": "سیشن برآمد کر دیا گیا",
"toast.session.export.success.description": "سیشن کو \u2068{{filename}}\u2069 میں محفوظ کر دیا گیا",
"toast.session.export.failed.title": "سیشن برآمد کرنے میں ناکام",
"toast.session.export.failed.description": "سیشن برآمد کرتے وقت ایک خرابی پیش آ گئی",
"toast.session.listFailed.title": "{{project}} کے لیے سیشن لوڈ کرنے میں ناکام", "toast.session.listFailed.title": "{{project}} کے لیے سیشن لوڈ کرنے میں ناکام",
"toast.project.reloadFailed.title": "{{project}} کو دوبارہ لوڈ کرنے میں ناکام", "toast.project.reloadFailed.title": "{{project}} کو دوبارہ لوڈ کرنے میں ناکام",
"toast.update.title": "اپ ڈیٹ دستیاب ہے۔", "toast.update.title": "اپ ڈیٹ دستیاب ہے۔",
@@ -834,6 +846,7 @@ export const dict = {
"common.moreOptions": "مزید اختیارات", "common.moreOptions": "مزید اختیارات",
"common.learnMore": "مزید جانیں", "common.learnMore": "مزید جانیں",
"common.rename": "نام تبدیل کریں۔", "common.rename": "نام تبدیل کریں۔",
"common.export": "برآمد کریں",
"common.reset": "دوبارہ ترتیب دیں۔", "common.reset": "دوبارہ ترتیب دیں۔",
"common.archive": "آرکائیو کریں", "common.archive": "آرکائیو کریں",
"common.delete": "حذف کریں۔", "common.delete": "حذف کریں۔",
File diff suppressed because it is too large Load Diff
+13
View File
@@ -185,6 +185,9 @@ export const dict = {
"command.session.share.description": "Chia sẻ phiên này và sao chép URL vào bảng tạm", "command.session.share.description": "Chia sẻ phiên này và sao chép URL vào bảng tạm",
"command.session.unshare": "Ngừng chia sẻ phiên", "command.session.unshare": "Ngừng chia sẻ phiên",
"command.session.unshare.description": "Dừng chia sẻ phiên này", "command.session.unshare.description": "Dừng chia sẻ phiên này",
"command.session.export": "Xuất phiên",
"command.session.export.description": "Xuất toàn bộ bản ghi phiên dưới dạng JSON",
"palette.search.placeholder": "Tìm kiếm tệp, lệnh và phiên", "palette.search.placeholder": "Tìm kiếm tệp, lệnh và phiên",
"palette.search.placeholder.home": "Tìm kiếm lệnh và phiên", "palette.search.placeholder.home": "Tìm kiếm lệnh và phiên",
"palette.empty": "Không tìm thấy kết quả nào", "palette.empty": "Không tìm thấy kết quả nào",
@@ -543,6 +546,8 @@ export const dict = {
"dialog.releaseNotes.action.next": "Tiếp theo", "dialog.releaseNotes.action.next": "Tiếp theo",
"dialog.releaseNotes.action.hideFuture": "Không hiển thị lại", "dialog.releaseNotes.action.hideFuture": "Không hiển thị lại",
"dialog.releaseNotes.media.alt": "Xem trước bản phát hành", "dialog.releaseNotes.media.alt": "Xem trước bản phát hành",
"dialog.usageExceeded.dontShowAgain": "Không hiển thị lại",
"context.breakdown.title": "Phân tích ngữ cảnh", "context.breakdown.title": "Phân tích ngữ cảnh",
"context.breakdown.note": "context.breakdown.note":
'Phân tích gần đúng của token đầu vào. "Khác" bao gồm các định nghĩa công cụ và chi phí chung.', 'Phân tích gần đúng của token đầu vào. "Khác" bao gồm các định nghĩa công cụ và chi phí chung.',
@@ -553,6 +558,8 @@ export const dict = {
"context.breakdown.other": "Khác", "context.breakdown.other": "Khác",
"context.systemPrompt.title": "Lời nhắc hệ thống", "context.systemPrompt.title": "Lời nhắc hệ thống",
"context.rawMessages.title": "Tin nhắn thô", "context.rawMessages.title": "Tin nhắn thô",
"context.export.session": "Xuất phiên",
"context.stats.session": "Phiên", "context.stats.session": "Phiên",
"context.stats.messages": "Tin nhắn", "context.stats.messages": "Tin nhắn",
"context.stats.provider": "Nhà cung cấp", "context.stats.provider": "Nhà cung cấp",
@@ -619,6 +626,11 @@ export const dict = {
"toast.session.unshare.success.description": "Đã hủy chia sẻ phiên thành công!", "toast.session.unshare.success.description": "Đã hủy chia sẻ phiên thành công!",
"toast.session.unshare.failed.title": "Không hủy chia sẻ được phiên", "toast.session.unshare.failed.title": "Không hủy chia sẻ được phiên",
"toast.session.unshare.failed.description": "Đã xảy ra lỗi khi hủy chia sẻ phiên", "toast.session.unshare.failed.description": "Đã xảy ra lỗi khi hủy chia sẻ phiên",
"toast.session.export.success.title": "Đã xuất phiên",
"toast.session.export.success.description": "Đã lưu phiên vào {{filename}}",
"toast.session.export.failed.title": "Không thể xuất phiên",
"toast.session.export.failed.description": "Đã xảy ra lỗi khi xuất phiên",
"toast.session.listFailed.title": "Không thể tải phiên cho {{project}}", "toast.session.listFailed.title": "Không thể tải phiên cho {{project}}",
"toast.project.reloadFailed.title": "Không thể tải lại {{project}}", "toast.project.reloadFailed.title": "Không thể tải lại {{project}}",
"toast.update.title": "Đã có bản cập nhật", "toast.update.title": "Đã có bản cập nhật",
@@ -834,6 +846,7 @@ export const dict = {
"common.moreOptions": "Nhiều lựa chọn hơn", "common.moreOptions": "Nhiều lựa chọn hơn",
"common.learnMore": "Tìm hiểu thêm", "common.learnMore": "Tìm hiểu thêm",
"common.rename": "Đổi tên", "common.rename": "Đổi tên",
"common.export": "Xuất",
"common.reset": "Đặt lại", "common.reset": "Đặt lại",
"common.archive": "Lưu trữ", "common.archive": "Lưu trữ",
"common.delete": "Xóa", "common.delete": "Xóa",
+20 -7
View File
@@ -218,6 +218,9 @@ export const dict = {
"command.session.unshare": "取消分享会话", "command.session.unshare": "取消分享会话",
"command.session.unshare.description": "停止分享此会话", "command.session.unshare.description": "停止分享此会话",
"command.session.export": "导出会话",
"command.session.export.description": "将完整会话记录导出为 JSON",
"palette.search.placeholder": "搜索文件、命令和会话", "palette.search.placeholder": "搜索文件、命令和会话",
"palette.search.placeholder.home": "搜索命令和会话", "palette.search.placeholder.home": "搜索命令和会话",
"palette.empty": "未找到结果", "palette.empty": "未找到结果",
@@ -577,8 +580,10 @@ export const dict = {
"dialog.project.edit.worktree.startup.description": "创建新工作区 (worktree) 后运行。", "dialog.project.edit.worktree.startup.description": "创建新工作区 (worktree) 后运行。",
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install", "dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
"dialog.usageExceeded.dontShowAgain": "不再显示",
"context.breakdown.title": "上下文细分", "context.breakdown.title": "上下文细分",
"context.breakdown.note": "输入令牌的大致细分。“其他”包含工具定义和开销。", "context.breakdown.note": "输入词元的大致细分。“其他”包含工具定义和开销。",
"context.breakdown.system": "系统", "context.breakdown.system": "系统",
"context.breakdown.user": "用户", "context.breakdown.user": "用户",
"context.breakdown.assistant": "助手", "context.breakdown.assistant": "助手",
@@ -586,23 +591,25 @@ export const dict = {
"context.breakdown.other": "其他", "context.breakdown.other": "其他",
"context.systemPrompt.title": "系统提示词", "context.systemPrompt.title": "系统提示词",
"context.rawMessages.title": "原始消息", "context.rawMessages.title": "原始消息",
"context.export.session": "导出会话",
"context.stats.session": "会话", "context.stats.session": "会话",
"context.stats.messages": "消息数", "context.stats.messages": "消息数",
"context.stats.provider": "提供商", "context.stats.provider": "提供商",
"context.stats.model": "模型", "context.stats.model": "模型",
"context.stats.limit": "上下文限制", "context.stats.limit": "上下文限制",
"context.stats.totalTokens": "令牌总数", "context.stats.totalTokens": "词元总数",
"context.stats.usage": "使用率", "context.stats.usage": "使用率",
"context.stats.inputTokens": "输入令牌", "context.stats.inputTokens": "输入词元",
"context.stats.outputTokens": "输出令牌", "context.stats.outputTokens": "输出词元",
"context.stats.reasoningTokens": "推理令牌", "context.stats.reasoningTokens": "推理词元",
"context.stats.cacheTokens": "缓存令牌(读/写)", "context.stats.cacheTokens": "缓存词元(读/写)",
"context.stats.userMessages": "用户消息", "context.stats.userMessages": "用户消息",
"context.stats.assistantMessages": "助手消息", "context.stats.assistantMessages": "助手消息",
"context.stats.totalCost": "总成本", "context.stats.totalCost": "总成本",
"context.stats.sessionCreated": "创建时间", "context.stats.sessionCreated": "创建时间",
"context.stats.lastActivity": "最后活动", "context.stats.lastActivity": "最后活动",
"context.usage.tokens": "令牌数", "context.usage.tokens": "词元数",
"context.usage.usage": "使用率", "context.usage.usage": "使用率",
"context.usage.cost": "成本", "context.usage.cost": "成本",
"context.usage.clickToView": "点击查看上下文", "context.usage.clickToView": "点击查看上下文",
@@ -654,6 +661,11 @@ export const dict = {
"toast.session.unshare.success.description": "会话已成功取消分享", "toast.session.unshare.success.description": "会话已成功取消分享",
"toast.session.unshare.failed.title": "取消分享失败", "toast.session.unshare.failed.title": "取消分享失败",
"toast.session.unshare.failed.description": "取消分享会话时发生错误", "toast.session.unshare.failed.description": "取消分享会话时发生错误",
"toast.session.export.success.title": "会话已导出",
"toast.session.export.success.description": "已将会话保存到 {{filename}}",
"toast.session.export.failed.title": "导出会话失败",
"toast.session.export.failed.description": "导出会话时发生错误",
"toast.session.listFailed.title": "无法加载 {{project}} 的会话", "toast.session.listFailed.title": "无法加载 {{project}} 的会话",
"toast.update.title": "有可用更新", "toast.update.title": "有可用更新",
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。", "toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
@@ -839,6 +851,7 @@ export const dict = {
"common.moreOptions": "更多选项", "common.moreOptions": "更多选项",
"common.learnMore": "了解更多", "common.learnMore": "了解更多",
"common.rename": "重命名", "common.rename": "重命名",
"common.export": "导出",
"common.reset": "重置", "common.reset": "重置",
"common.archive": "归档", "common.archive": "归档",
"common.delete": "删除", "common.delete": "删除",
+13
View File
@@ -195,6 +195,9 @@ export const dict = {
"command.session.unshare": "取消分享工作階段", "command.session.unshare": "取消分享工作階段",
"command.session.unshare.description": "停止分享此工作階段", "command.session.unshare.description": "停止分享此工作階段",
"command.session.export": "匯出工作階段",
"command.session.export.description": "將完整的工作階段記錄匯出為 JSON",
"palette.search.placeholder": "搜尋檔案、命令和工作階段", "palette.search.placeholder": "搜尋檔案、命令和工作階段",
"palette.search.placeholder.home": "搜尋命令和工作階段", "palette.search.placeholder.home": "搜尋命令和工作階段",
"palette.empty": "找不到結果", "palette.empty": "找不到結果",
@@ -558,6 +561,8 @@ export const dict = {
"dialog.project.edit.worktree.startup": "工作區啟動腳本", "dialog.project.edit.worktree.startup": "工作區啟動腳本",
"dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。", "dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。",
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install", "dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
"dialog.usageExceeded.dontShowAgain": "不再顯示",
"context.breakdown.title": "上下文細分", "context.breakdown.title": "上下文細分",
"context.breakdown.note": "輸入 token 的概略細分。「其他」包含工具定義和額外負擔。", "context.breakdown.note": "輸入 token 的概略細分。「其他」包含工具定義和額外負擔。",
"context.breakdown.system": "系統", "context.breakdown.system": "系統",
@@ -569,6 +574,8 @@ export const dict = {
"context.systemPrompt.title": "系統提示詞", "context.systemPrompt.title": "系統提示詞",
"context.rawMessages.title": "原始訊息", "context.rawMessages.title": "原始訊息",
"context.export.session": "匯出工作階段",
"context.stats.session": "工作階段", "context.stats.session": "工作階段",
"context.stats.messages": "訊息數", "context.stats.messages": "訊息數",
"context.stats.provider": "提供者", "context.stats.provider": "提供者",
@@ -646,6 +653,11 @@ export const dict = {
"toast.session.unshare.failed.title": "取消分享失敗", "toast.session.unshare.failed.title": "取消分享失敗",
"toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤", "toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤",
"toast.session.export.success.title": "工作階段已匯出",
"toast.session.export.success.description": "已將工作階段儲存至 {{filename}}",
"toast.session.export.failed.title": "匯出工作階段失敗",
"toast.session.export.failed.description": "匯出工作階段時發生錯誤",
"toast.session.listFailed.title": "無法載入 {{project}} 的工作階段", "toast.session.listFailed.title": "無法載入 {{project}} 的工作階段",
"toast.update.title": "有可用更新", "toast.update.title": "有可用更新",
@@ -836,6 +848,7 @@ export const dict = {
"common.moreOptions": "更多選項", "common.moreOptions": "更多選項",
"common.learnMore": "深入了解", "common.learnMore": "深入了解",
"common.rename": "重新命名", "common.rename": "重新命名",
"common.export": "匯出",
"common.reset": "重設", "common.reset": "重設",
"common.archive": "封存", "common.archive": "封存",
"common.delete": "刪除", "common.delete": "刪除",
+14 -1
View File
@@ -90,7 +90,20 @@ export function createHomeController() {
const directory = directories[0] const directory = directories[0]
if (!directory) return if (!directory) return
const ctx = global.ensureServerCtx(conn) const ctx = global.ensureServerCtx(conn)
directories.forEach((item) => ctx.projects.open(item)) directories.forEach((item) => {
if (ctx.projects.list().some((project) => project.worktree === item)) return
const location = { directory: item }
void ctx.sdk.api.file
.list({ path: ".", location })
.then(async (files) => {
if (files.data.length > 0) return ctx.sdk.api.project.current({ location })
const result = await ctx.sdk.client.project.initGit({ directory: item })
return result.data ?? ctx.sdk.api.project.current({ location })
})
.then((project) => ctx.sync.child(item, { bootstrap: false })[1]("project", project.id))
.catch(() => undefined)
ctx.projects.open(item)
})
ctx.projects.touch(directory) ctx.projects.touch(directory)
setSelection({ server: ServerConnection.key(conn), directory }) setSelection({ server: ServerConnection.key(conn), directory })
}, },
@@ -475,6 +475,10 @@ function HomeProjectRow(
ref={sortable.ref} ref={sortable.ref}
class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]" class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"
classList={{ "z-10": sortable.isDragSource() }} classList={{ "z-10": sortable.isDragSource() }}
onContextMenu={(event) => {
event.preventDefault()
props.onSetContextMenuOpen(contextMenuID(), true)
}}
> >
<HomeProjectNavButton <HomeProjectNavButton
type="button" type="button"
@@ -15,7 +15,7 @@ import type { LocalProject } from "@/context/layout"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/server" import { ServerConnection } from "@/context/server"
import { sessionHasOpenTab, useTabs } from "@/context/tabs" import { sessionHasOpenTab, useTabs } from "@/context/tabs"
import { displayName, errorMessage, projectForSession } from "@/pages/layout/helpers" import { compareSessionTime, displayName, errorMessage, projectForSession } from "@/pages/layout/helpers"
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
@@ -254,7 +254,7 @@ function buildHomeSessionRecords(input: {
const directories = new Set(input.projectDirectories().map(pathKey)) const directories = new Set(input.projectDirectories().map(pathKey))
const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory))) const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory)))
return [...new Map(sessions.map((session) => [session.id, session] as const)).values()] return [...new Map(sessions.map((session) => [session.id, session] as const)).values()]
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .sort(compareSessionTime)
.flatMap((session) => { .flatMap((session) => {
const directory = pathKey(session.directory) const directory = pathKey(session.directory)
const project = const project =
@@ -10,6 +10,7 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
import { import {
childSessionOnPath, childSessionOnPath,
closeHomeProject, closeHomeProject,
compareSessionTime,
displayName, displayName,
effectiveWorkspaceOrder, effectiveWorkspaceOrder,
errorMessage, errorMessage,
@@ -18,6 +19,7 @@ import {
homeProjectDirectories, homeProjectDirectories,
homeSessionServerStatus, homeSessionServerStatus,
latestRootSession, latestRootSession,
sortedRootSessions,
toggleHomeProjectSelection, toggleHomeProjectSelection,
} from "./helpers" } from "./helpers"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
@@ -153,6 +155,30 @@ describe("layout workspace helpers", () => {
expect(result?.id).toBe("workspace") expect(result?.id).toBe("workspace")
}) })
test("sorts recent sessions by persisted update time instead of id", () => {
const result = sortedRootSessions(
{
path: { directory: "/workspace" },
session: [
session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 3, archived: undefined } }),
],
},
3,
)
expect(result.map((item) => item.id)).toEqual(["ses_a", "ses_z"])
})
test("uses id only to break equal session timestamps", () => {
const sessions = [
session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }),
]
expect(sessions.sort(compareSessionTime).map((item) => item.id)).toEqual(["ses_a", "ses_z"])
})
test("detects project permissions with a filter", () => { test("detects project permissions with a filter", () => {
const result = hasProjectPermissions( const result = hasProjectPermissions(
{ {
+7 -15
View File
@@ -9,18 +9,10 @@ type SessionStore = {
path: { directory: string } path: { directory: string }
} }
function sortSessions(now: number) { export function compareSessionTime(a: Session, b: Session) {
const oneMinuteAgo = now - 60 * 1000 const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)
return (a: Session, b: Session) => { if (updated !== 0) return updated
const aUpdated = a.time.updated ?? a.time.created return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
const bUpdated = b.time.updated ?? b.time.created
const aRecent = aUpdated > oneMinuteAgo
const bRecent = bUpdated > oneMinuteAgo
if (aRecent && bRecent) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
if (aRecent && !bRecent) return -1
if (!aRecent && bRecent) return 1
return bUpdated - aUpdated
}
} }
const isRootVisibleSession = (session: Session, directory: string) => const isRootVisibleSession = (session: Session, directory: string) =>
@@ -29,10 +21,10 @@ const isRootVisibleSession = (session: Session, directory: string) =>
export const roots = (store: SessionStore) => export const roots = (store: SessionStore) =>
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now)) export const sortedRootSessions = (store: SessionStore, _now: number) => roots(store).sort(compareSessionTime)
export const latestRootSession = (stores: SessionStore[], now: number) => export const latestRootSession = (stores: SessionStore[], _now: number) =>
stores.flatMap(roots).sort(sortSessions(now))[0] stores.flatMap(roots).sort(compareSessionTime)[0]
export function hasProjectPermissions<T>( export function hasProjectPermissions<T>(
request: Record<string, T[] | undefined> | undefined, request: Record<string, T[] | undefined> | undefined,
+6 -2
View File
@@ -1851,7 +1851,9 @@ export default function Page() {
const session = sdk().api.session const session = sdk().api.session
const target = sync() const target = sync()
const next = userMessages().find((item) => item.id > id) const index = userMessages().findIndex((item) => item.id === id)
if (index < 0) return
const next = userMessages()[index + 1]
const last = target.session.get(sessionID)?.revert const last = target.session.get(sessionID)?.revert
await runPromptRollbackMutation({ await runPromptRollbackMutation({
@@ -1891,8 +1893,10 @@ export default function Page() {
const rolled = createMemo(() => { const rolled = createMemo(() => {
const id = revertMessageID() const id = revertMessageID()
if (!id) return [] if (!id) return []
const index = userMessages().findIndex((item) => item.id === id)
if (index < 0) return []
return userMessages() return userMessages()
.filter((item) => item.id >= id) .slice(index)
.map((item) => ({ id: item.id, text: line(item.id) })) .map((item) => ({ id: item.id, text: line(item.id) }))
}) })
@@ -53,6 +53,7 @@ import type {
UserMessage, UserMessage,
} from "@opencode-ai/sdk/v2" } from "@opencode-ai/sdk/v2"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover" import { Popover as KobaltePopover } from "@kobalte/core/popover"
import { normalize } from "@opencode-ai/session-ui/session-diff" import { normalize } from "@opencode-ai/session-ui/session-diff"
@@ -286,7 +287,9 @@ export function MessageTimeline(props: {
const visible = new Set(props.userMessages.map((message) => message.id)) const visible = new Set(props.userMessages.map((message) => message.id))
const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
const messages = sync().data.session_message[id] ?? [] const messages = sync().data.session_message[id] ?? []
return boundary ? messages.filter((message) => message.id < boundary) : messages if (!boundary) return messages
const index = messages.findIndex((message) => message.id === boundary)
return index < 0 ? messages : messages.slice(0, index)
}) })
const info = createMemo(() => { const info = createMemo(() => {
const id = sessionID() const id = sessionID()
@@ -806,6 +809,29 @@ export function MessageTimeline(props: {
navigate(`/${params.dir}/session`) navigate(`/${params.dir}/session`)
} }
const exportSession = async (sessionID: string) => {
try {
const data = await fetchSessionExport({
sessionID,
client: sdk().client,
})
const filename = sessionExportFilename(data.info)
downloadSessionExport(filename, data)
showToast({
variant: "success",
icon: "circle-check",
title: language.t("toast.session.export.success.title"),
description: language.t("toast.session.export.success.description", { filename }),
})
} catch (err) {
showToast({
variant: "error",
title: language.t("toast.session.export.failed.title"),
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
})
}
}
const archiveSession = async (sessionID: string) => { const archiveSession = async (sessionID: string) => {
const session = sync().session.get(sessionID) const session = sync().session.get(sessionID)
if (!session) return if (!session) return
@@ -1564,6 +1590,9 @@ export function MessageTimeline(props: {
</DropdownMenu.ItemLabel> </DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
</Show> </Show>
<DropdownMenu.Item onSelect={() => exportSession(id)}>
<DropdownMenu.ItemLabel>{language.t("common.export")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => void archiveSession(id)}> <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -1635,6 +1664,9 @@ export function MessageTimeline(props: {
{language.t("session.share.action.share")}... {language.t("session.share.action.share")}...
</MenuV2.Item> </MenuV2.Item>
</Show> </Show>
<MenuV2.Item onSelect={() => exportSession(id)}>
{language.t("common.export")}...
</MenuV2.Item>
<MenuV2.Item onSelect={() => void archiveSession(id)}> <MenuV2.Item onSelect={() => void archiveSession(id)}>
{language.t("common.archive")} {language.t("common.archive")}
</MenuV2.Item> </MenuV2.Item>
@@ -7,11 +7,11 @@ const assistant = (id: string) => ({ id, role: "assistant" }) as AssistantMessag
describe("timeline model", () => { describe("timeline model", () => {
test("selects users and applies the revert boundary", () => { test("selects users and applies the revert boundary", () => {
const messages: Message[] = [user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_5")] const messages: Message[] = [user("msg_z"), assistant("msg_a"), user("msg_b"), user("msg_c")]
const users = selectUserMessages(messages) const users = selectUserMessages(messages)
expect(users.map((message) => message.id)).toEqual(["msg_1", "msg_3", "msg_5"]) expect(users.map((message) => message.id)).toEqual(["msg_z", "msg_b", "msg_c"])
expect(selectVisibleUserMessages(users, "msg_5").map((message) => message.id)).toEqual(["msg_1", "msg_3"]) expect(selectVisibleUserMessages(users, "msg_b").map((message) => message.id)).toEqual(["msg_z"])
expect(selectVisibleUserMessages(users)).toBe(users) expect(selectVisibleUserMessages(users)).toBe(users)
}) })
@@ -104,7 +104,8 @@ export function isTimelineReady(messages: Message[] | undefined, loading: boolea
export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) { export function selectVisibleUserMessages(messages: UserMessage[], revertMessageID?: string) {
if (!revertMessageID) return messages if (!revertMessageID) return messages
return messages.filter((message) => message.id < revertMessageID) const boundary = messages.findIndex((message) => message.id === revertMessageID)
return boundary < 0 ? messages : messages.slice(0, boundary)
} }
export async function loadOlderTimeline(input: { export async function loadOlderTimeline(input: {
@@ -137,11 +137,11 @@ describe("current session timeline rows", () => {
test("renders an optimistic user turn and thinking before the protocol message arrives", () => { test("renders an optimistic user turn and thinking before the protocol message arrives", () => {
const source = [ const source = [
{ id: "msg_1", type: "user", text: "existing", time: { created: 1 } }, { id: "msg_z", type: "user", text: "existing", time: { created: 1 } },
] satisfies SessionMessageInfo[] ] satisfies SessionMessageInfo[]
const normalized = normalizeSessionMessages("ses_1", source) const normalized = normalizeSessionMessages("ses_1", source)
const optimistic = { const optimistic = {
id: "msg_2", id: "msg_a",
sessionID: "ses_1", sessionID: "ses_1",
role: "user" as const, role: "user" as const,
time: { created: 2 }, time: { created: 2 },
@@ -161,10 +161,10 @@ describe("current session timeline rows", () => {
expect(result.activeMessageID).toBe(optimistic.id) expect(result.activeMessageID).toBe(optimistic.id)
expect(result.rows.map(TimelineRow.key)).toEqual([ expect(result.rows.map(TimelineRow.key)).toEqual([
"user-message:msg_1", "user-message:msg_z",
"turn-gap:msg_2", "turn-gap:msg_a",
"user-message:msg_2", "user-message:msg_a",
"thinking:msg_2", "thinking:msg_a",
]) ])
}) })

Some files were not shown because too many files have changed in this diff Show More