mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 09:39:46 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5562652f63 | |||
| 031510e1c3 |
@@ -322,7 +322,6 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -449,7 +449,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +509,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +559,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -690,7 +690,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +766,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +796,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -836,7 +836,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -849,7 +849,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -883,7 +883,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -902,7 +902,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -944,7 +944,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -971,7 +971,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1022,7 +1022,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1078,7 +1078,6 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
|
||||
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
|
||||
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
|
||||
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
|
||||
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=",
|
||||
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=",
|
||||
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=",
|
||||
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -158,7 +158,6 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch"
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
||||
- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts.
|
||||
- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code.
|
||||
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
## 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,9 +197,7 @@ export async function setupTimeline(
|
||||
)
|
||||
},
|
||||
async waitForPart(partID: string) {
|
||||
const part = page.locator(`[data-timeline-part-id="${partID}"]`)
|
||||
await expect(part).toHaveCount(1)
|
||||
await expect(part).toBeVisible()
|
||||
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ test("opens the comment editor when code is clicked", async ({ page }) => {
|
||||
await line.click()
|
||||
|
||||
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 }) => {
|
||||
@@ -28,7 +27,6 @@ test("opens the comment editor when a line number is clicked", async ({ page })
|
||||
await lineNumber.click()
|
||||
|
||||
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 }) => {
|
||||
@@ -38,10 +36,15 @@ test("opens the comment editor for a line number range", async ({ page }) => {
|
||||
await expectAppVisible(start)
|
||||
await expectAppVisible(end)
|
||||
|
||||
await start.dragTo(end)
|
||||
const from = await start.boundingBox()
|
||||
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.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 }) => {
|
||||
@@ -51,38 +54,31 @@ test("shows a comment button when a line number is hovered", async ({ page }) =>
|
||||
|
||||
const comment = review.getByRole("button", { name: "Comment", exact: true })
|
||||
await expect(async () => {
|
||||
await page.mouse.move(0, 0)
|
||||
await lineNumber.hover()
|
||||
await expect(lineNumber).toHaveAttribute("data-hovered", "")
|
||||
await expect(comment).toHaveCount(1)
|
||||
await expect(comment).toHaveCSS("pointer-events", "auto")
|
||||
await comment.focus()
|
||||
await expect(comment).toBeFocused()
|
||||
}).toPass({ timeout: 10_000 })
|
||||
await comment.press("Enter")
|
||||
await expect(comment).toBeVisible({ timeout: 500 })
|
||||
await comment.click({ timeout: 500 })
|
||||
}).toPass()
|
||||
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 }) => {
|
||||
const requests: string[] = []
|
||||
page.on("request", (request) => {
|
||||
expect.soft(request.method(), `unexpected ${request.method()} ${new URL(request.url()).pathname}`).toBe("GET")
|
||||
if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`)
|
||||
})
|
||||
|
||||
const review = page.locator('[data-component="session-review"]')
|
||||
await review.getByText("export const value = 'after'", { exact: true }).click()
|
||||
const textbox = review.getByRole("textbox")
|
||||
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 review.getByRole("textbox").fill("Use the existing value instead")
|
||||
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click()
|
||||
|
||||
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
|
||||
await page.getByRole("tab", { name: "Session" }).click()
|
||||
const context = page.getByText("Use the existing value instead", { exact: true }).last()
|
||||
await expect(context).toBeVisible()
|
||||
await expect(context.locator("..")).toContainText("review.ts:2")
|
||||
expect(requests).toEqual([])
|
||||
})
|
||||
|
||||
async function openReview(page: Page) {
|
||||
@@ -148,22 +144,15 @@ async function openReview(page: Page) {
|
||||
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await expectSessionTitle(page, title)
|
||||
const changes = page.getByRole("tab", { name: "Changes" })
|
||||
const diffResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === "GET" && response.ok() && new URL(response.url()).pathname === "/api/vcs/diff",
|
||||
)
|
||||
await changes.click()
|
||||
const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff")
|
||||
await page.getByRole("tab", { name: "Changes" }).click()
|
||||
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"]')
|
||||
await expectAppVisible(review)
|
||||
const file = review.locator('[data-file="src/review.ts"]')
|
||||
await expectAppVisible(file)
|
||||
const trigger = file.getByRole("button", { expanded: false })
|
||||
await expect(trigger).toHaveCount(1)
|
||||
await trigger.click()
|
||||
await expect(file.getByRole("button", { expanded: true })).toBeVisible()
|
||||
await expect(file.getByText("export const value = 'after'", { exact: true })).toBeVisible()
|
||||
await review
|
||||
.getByRole("heading", { name: /review\.ts/ })
|
||||
.getByRole("button")
|
||||
.first()
|
||||
.click()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
setupTimeline,
|
||||
status,
|
||||
textPart,
|
||||
userMessage,
|
||||
} from "../performance/timeline-stability/fixture"
|
||||
@@ -16,7 +17,7 @@ test("keeps one connection open while delivering multiple events", async ({ page
|
||||
await timeline.waitForPart("prt_transport_first")
|
||||
await timeline.waitForPart("prt_transport_second")
|
||||
expect(first.connectionID).toBe(second.connectionID)
|
||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
||||
expect(await timeline.transport.connections()).toHaveLength(1)
|
||||
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
|
||||
})
|
||||
|
||||
@@ -50,28 +51,20 @@ test("parses split JSON and a split multibyte code point", async ({ page }) => {
|
||||
})
|
||||
|
||||
test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
|
||||
const sentinelID = "prt_transport_heartbeat_sentinel"
|
||||
const timeline = await setupTimeline(page, {
|
||||
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
|
||||
})
|
||||
await timeline.waitForPart("prt_transport_steady")
|
||||
const before = await stableTimelineRows(page)
|
||||
const before = await page.locator("[data-timeline-row]").allTextContents()
|
||||
|
||||
await timeline.transport.writeRaw(": heartbeat\n\n")
|
||||
await timeline.transport.send(partUpdated(textPart(sentinelID, "heartbeat processed")))
|
||||
await timeline.waitForPart(sentinelID)
|
||||
await timeline.transport.heartbeat()
|
||||
await timeline.settle()
|
||||
|
||||
await expect
|
||||
.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)
|
||||
expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
|
||||
expect(await timeline.transport.connections()).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("reconnects after a clean close", async ({ page }) => {
|
||||
const timeline = await setupTimeline(page)
|
||||
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
||||
const first = await timeline.transport.waitForConnection()
|
||||
|
||||
await timeline.transport.close()
|
||||
@@ -84,21 +77,20 @@ test("reconnects after a clean close", async ({ page }) => {
|
||||
})
|
||||
|
||||
test("reconnects after a stream error", async ({ page }) => {
|
||||
const timeline = await setupTimeline(page)
|
||||
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
||||
const first = await timeline.transport.waitForConnection()
|
||||
|
||||
await timeline.transport.error("contract failure")
|
||||
const second = await timeline.transport.waitForConnection({ after: first.id })
|
||||
await timeline.transport.send(partUpdated(textPart("prt_transport_error", "after error")))
|
||||
await timeline.transport.send(status("busy"))
|
||||
|
||||
await timeline.waitForPart("prt_transport_error")
|
||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
|
||||
expect(second.id).toBeGreaterThan(first.id)
|
||||
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
|
||||
})
|
||||
|
||||
test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
|
||||
const timeline = await setupTimeline(page, { protocol: "v2" })
|
||||
const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" })
|
||||
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
|
||||
id: "timeline-event-7",
|
||||
})
|
||||
@@ -120,35 +112,5 @@ test("passes through non-event fetches", async ({ page }) => {
|
||||
})
|
||||
|
||||
expect(health).toEqual({ healthy: true })
|
||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
||||
expect(await timeline.transport.connections()).toHaveLength(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,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -247,23 +247,18 @@ export async function installSseTransport<T>(
|
||||
return {
|
||||
server,
|
||||
async waitForConnection(input = {}) {
|
||||
const connection = await page.waitForFunction(
|
||||
await page.waitForFunction(
|
||||
(after) => {
|
||||
const transport = (window as BrowserTransport).__testSseTransport
|
||||
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
|
||||
return connections?.findLast((connection) => connection.id > after && connection.endedAt === undefined)
|
||||
return connections?.some((connection) => connection.id > after)
|
||||
},
|
||||
input.after ?? 0,
|
||||
{ timeout: input.timeout },
|
||||
)
|
||||
let result: SseConnectionRecord | undefined
|
||||
try {
|
||||
result = await connection.jsonValue()
|
||||
} finally {
|
||||
await connection.dispose()
|
||||
}
|
||||
if (!result) throw new Error("SSE transport connection disappeared while waiting")
|
||||
return result
|
||||
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast(
|
||||
(connection) => connection.id > (input.after ?? 0),
|
||||
)!
|
||||
},
|
||||
send(payload, eventOptions) {
|
||||
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
@@ -15,7 +14,6 @@ export type DialogGoUpsellProps = {
|
||||
|
||||
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
const runAction = () => {
|
||||
@@ -34,7 +32,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={dismiss}>
|
||||
{language.t("dialog.usageExceeded.dontShowAgain")}
|
||||
Don't show again
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={runAction}>
|
||||
{props.actionLabel}
|
||||
|
||||
@@ -5,15 +5,12 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { File } from "@opencode-ai/session-ui/file"
|
||||
import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -223,31 +220,6 @@ export function SessionContextTab() {
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
] satisfies { label: string; value: () => JSX.Element }[]
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
@@ -356,18 +328,7 @@ export function SessionContextTab() {
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="gap-1.5 px-2 text-text-weak hover:text-text-base"
|
||||
onClick={exportSession}
|
||||
>
|
||||
<Icon name="download" size="small" />
|
||||
<span>{language.t("context.export.session")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Accordion multiple>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
|
||||
@@ -628,7 +628,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||
aria-label={props.state.ariaLabel}
|
||||
>
|
||||
<span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0">
|
||||
{props.state.label}
|
||||
Update
|
||||
</span>
|
||||
<span class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Show
|
||||
|
||||
@@ -9,7 +9,6 @@ import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import {
|
||||
createDesktopNativeBundle,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
type DesktopNativeBundle,
|
||||
type DesktopNativeLocale,
|
||||
@@ -70,6 +69,40 @@ const INTL: Record<Locale, string> = {
|
||||
sv: "sv-SE",
|
||||
}
|
||||
|
||||
const LABEL_KEY: Partial<Record<Locale, keyof Dictionary>> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
zht: "language.zht",
|
||||
ko: "language.ko",
|
||||
de: "language.de",
|
||||
es: "language.es",
|
||||
fr: "language.fr",
|
||||
da: "language.da",
|
||||
ja: "language.ja",
|
||||
pl: "language.pl",
|
||||
ru: "language.ru",
|
||||
uk: "language.uk",
|
||||
ar: "language.ar",
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
tr: "language.tr",
|
||||
}
|
||||
|
||||
const LABEL: Partial<Record<Locale, string>> = {
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
}
|
||||
|
||||
const base = i18n.flatten({ ...en, ...uiEn })
|
||||
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
||||
|
||||
@@ -239,7 +272,11 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
|
||||
}
|
||||
|
||||
const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
|
||||
const label = (value: Locale) => {
|
||||
const key = LABEL_KEY[value]
|
||||
if (key) return t(key)
|
||||
return LABEL[value] ?? value
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document !== "object") return
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
"command.session.export": "تصدير الجلسة",
|
||||
"command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON",
|
||||
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
@@ -534,8 +531,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "برنامج نصي لبدء تشغيل مساحة العمل",
|
||||
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "عدم الإظهار مرة أخرى",
|
||||
|
||||
"context.breakdown.title": "تفصيل السياق",
|
||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال المميزة. يشمل "أخرى" تعريفات الأدوات والأعباء الإضافية.',
|
||||
"context.breakdown.system": "النظام",
|
||||
@@ -545,8 +540,6 @@ export const dict = {
|
||||
"context.breakdown.other": "أخرى",
|
||||
"context.systemPrompt.title": "موجّه النظام",
|
||||
"context.rawMessages.title": "الرسائل الخام",
|
||||
"context.export.session": "تصدير الجلسة",
|
||||
|
||||
"context.stats.session": "جلسة",
|
||||
"context.stats.messages": "رسائل",
|
||||
"context.stats.provider": "موفر",
|
||||
@@ -613,11 +606,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
|
||||
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
|
||||
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
|
||||
"toast.session.export.success.title": "تم تصدير الجلسة",
|
||||
"toast.session.export.success.description": "تم حفظ الجلسة في \u2068{{filename}}\u2069",
|
||||
"toast.session.export.failed.title": "فشل تصدير الجلسة",
|
||||
"toast.session.export.failed.description": "حدث خطأ أثناء تصدير الجلسة",
|
||||
|
||||
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
|
||||
"toast.update.title": "تحديث متاح",
|
||||
"toast.update.description": "يتوفر الآن إصدار جديد من OpenCode ({{version}}) للتثبيت.",
|
||||
@@ -805,7 +793,6 @@ export const dict = {
|
||||
"common.moreOptions": "مزيد من الخيارات",
|
||||
"common.learnMore": "اطّلع على المزيد",
|
||||
"common.rename": "إعادة تسمية",
|
||||
"common.export": "تصدير",
|
||||
"common.reset": "إعادة تعيين",
|
||||
"common.archive": "أرشفة",
|
||||
"common.delete": "حذف",
|
||||
|
||||
@@ -180,9 +180,6 @@ export const dict = {
|
||||
"command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala",
|
||||
"command.session.unshare": "Sessiyanın paylaşımını dayandır",
|
||||
"command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır",
|
||||
"command.session.export": "Sessiyanı ixrac et",
|
||||
"command.session.export.description": "Sessiyanın tam transkriptini JSON formatında ixrac et",
|
||||
|
||||
"palette.search.placeholder": "Fayl, əmr və sessiya axtar",
|
||||
"palette.search.placeholder.home": "Əmr və sessiyaları axtar",
|
||||
"palette.empty": "Nəticə tapılmadı",
|
||||
@@ -544,8 +541,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Növbəti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Gələcəkdə göstərmə",
|
||||
"dialog.releaseNotes.media.alt": "Buraxılış önbaxışı",
|
||||
"dialog.usageExceeded.dontShowAgain": "Bir daha göstərmə",
|
||||
|
||||
"context.breakdown.title": "Kontekst bölgüsü",
|
||||
"context.breakdown.note": 'Giriş tokenlərinin təxmini bölgüsü. "Digər" alət təriflərini və əlavə yükü əhatə edir.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -555,8 +550,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Digər",
|
||||
"context.systemPrompt.title": "Sistem promptu",
|
||||
"context.rawMessages.title": "Xam mesajlar",
|
||||
"context.export.session": "Sessiyanı ixrac et",
|
||||
|
||||
"context.stats.session": "Sessiya",
|
||||
"context.stats.messages": "Mesajlar",
|
||||
"context.stats.provider": "Provayder",
|
||||
@@ -623,11 +616,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!",
|
||||
"toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi",
|
||||
"toast.session.unshare.failed.description": "Sessiyanın paylaşımını dayandırarkən xəta baş verdi",
|
||||
"toast.session.export.success.title": "Sessiya ixrac edildi",
|
||||
"toast.session.export.success.description": "Sessiya {{filename}} faylına saxlanıldı",
|
||||
"toast.session.export.failed.title": "Sessiya ixrac edilə bilmədi",
|
||||
"toast.session.export.failed.description": "Sessiyanı ixrac edərkən xəta baş verdi",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} üçün sessiyalar yüklənə bilmədi",
|
||||
"toast.project.reloadFailed.title": "{{project}} yenidən yüklənə bilmədi",
|
||||
"toast.update.title": "Yeniləmə mövcuddur",
|
||||
@@ -844,7 +832,6 @@ export const dict = {
|
||||
"common.moreOptions": "Daha çox seçim",
|
||||
"common.learnMore": "Ətraflı öyrən",
|
||||
"common.rename": "Adını dəyiş",
|
||||
"common.export": "İxrac et",
|
||||
"common.reset": "Sıfırla",
|
||||
"common.archive": "Arxivlə",
|
||||
"common.delete": "Sil",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
"command.session.export": "Exportar sessão",
|
||||
"command.session.export.description": "Exportar a transcrição completa da sessão como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
@@ -537,8 +534,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
|
||||
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "Ex.: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Não mostrar novamente",
|
||||
|
||||
"context.breakdown.title": "Detalhamento do contexto",
|
||||
"context.breakdown.note":
|
||||
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e sobrecarga.',
|
||||
@@ -549,8 +544,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Outros",
|
||||
"context.systemPrompt.title": "Prompt do sistema",
|
||||
"context.rawMessages.title": "Mensagens brutas",
|
||||
"context.export.session": "Exportar sessão",
|
||||
|
||||
"context.stats.session": "Sessão",
|
||||
"context.stats.messages": "Mensagens",
|
||||
"context.stats.provider": "Provedor",
|
||||
@@ -617,11 +610,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
|
||||
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
|
||||
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
|
||||
"toast.session.export.success.title": "Sessão exportada",
|
||||
"toast.session.export.success.description": "Sessão salva em {{filename}}",
|
||||
"toast.session.export.failed.title": "Falha ao exportar sessão",
|
||||
"toast.session.export.failed.description": "Ocorreu um erro ao exportar a sessão",
|
||||
|
||||
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
|
||||
"toast.update.title": "Atualização disponível",
|
||||
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
|
||||
@@ -808,7 +796,6 @@ export const dict = {
|
||||
"common.moreOptions": "Mais opções",
|
||||
"common.learnMore": "Saiba mais",
|
||||
"common.rename": "Renomear",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Redefinir",
|
||||
"common.archive": "Arquivar",
|
||||
"common.delete": "Excluir",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Ukini dijeljenje sesije",
|
||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||
|
||||
"command.session.export": "Izvezi sesiju",
|
||||
"command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu",
|
||||
|
||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||
"palette.empty": "Nema rezultata",
|
||||
@@ -568,8 +565,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Nemoj više prikazivati",
|
||||
|
||||
"context.breakdown.title": "Razlaganje konteksta",
|
||||
"context.breakdown.note":
|
||||
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatno opterećenje.',
|
||||
@@ -582,8 +577,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Sistemski prompt",
|
||||
"context.rawMessages.title": "Sirove poruke",
|
||||
|
||||
"context.export.session": "Izvezi sesiju",
|
||||
|
||||
"context.stats.session": "Sesija",
|
||||
"context.stats.messages": "Poruke",
|
||||
"context.stats.provider": "Provajder",
|
||||
@@ -662,11 +655,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
|
||||
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
|
||||
|
||||
"toast.session.export.success.title": "Sesija izvezena",
|
||||
"toast.session.export.success.description": "Sesija je sačuvana kao {{filename}}",
|
||||
"toast.session.export.failed.title": "Izvoz sesije nije uspio",
|
||||
"toast.session.export.failed.description": "Došlo je do greške prilikom izvoza sesije",
|
||||
|
||||
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
|
||||
|
||||
"toast.update.title": "Dostupno ažuriranje",
|
||||
@@ -868,7 +856,6 @@ export const dict = {
|
||||
"common.moreOptions": "Više opcija",
|
||||
"common.learnMore": "Saznaj više",
|
||||
"common.rename": "Preimenuj",
|
||||
"common.export": "Izvezi",
|
||||
"common.reset": "Vrati na početno stanje",
|
||||
"common.archive": "Arhiviraj",
|
||||
"common.delete": "Izbriši",
|
||||
|
||||
@@ -92,9 +92,6 @@ export const dict = {
|
||||
"command.session.unshare": "Stop deling af session",
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"command.session.export": "Eksportér session",
|
||||
"command.session.export.description": "Eksportér hele sessionsudskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||
"palette.search.placeholder.home": "Søg i kommandoer og sessioner",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
@@ -449,8 +446,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
|
||||
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Vis ikke igen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||
@@ -463,8 +458,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå beskeder",
|
||||
|
||||
"context.export.session": "Eksportér session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Beskeder",
|
||||
"context.stats.provider": "Udbyder",
|
||||
@@ -542,11 +535,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session",
|
||||
"toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling",
|
||||
|
||||
"toast.session.export.success.title": "Session eksporteret",
|
||||
"toast.session.export.success.description": "Sessionen blev gemt i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere session",
|
||||
"toast.session.export.failed.description": "Der opstod en fejl under eksport af sessionen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}",
|
||||
|
||||
"toast.update.title": "Opdatering tilgængelig",
|
||||
@@ -745,7 +733,6 @@ export const dict = {
|
||||
"common.moreOptions": "Flere muligheder",
|
||||
"common.learnMore": "Lær mere",
|
||||
"common.rename": "Omdøb",
|
||||
"common.export": "Eksportér",
|
||||
"common.reset": "Nulstil",
|
||||
"common.archive": "Arkivér",
|
||||
"common.delete": "Slet",
|
||||
|
||||
@@ -89,9 +89,6 @@ export const dict = {
|
||||
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
"command.session.export": "Sitzung exportieren",
|
||||
"command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren",
|
||||
|
||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
@@ -428,8 +425,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nicht mehr anzeigen",
|
||||
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||
@@ -440,8 +435,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Andere",
|
||||
"context.systemPrompt.title": "System-Prompt",
|
||||
"context.rawMessages.title": "Rohdaten der Nachrichten",
|
||||
"context.export.session": "Sitzung exportieren",
|
||||
|
||||
"context.stats.session": "Sitzung",
|
||||
"context.stats.messages": "Nachrichten",
|
||||
"context.stats.provider": "Anbieter",
|
||||
@@ -508,11 +501,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
|
||||
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
|
||||
"toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten",
|
||||
"toast.session.export.success.title": "Sitzung exportiert",
|
||||
"toast.session.export.success.description": "Sitzung unter {{filename}} gespeichert",
|
||||
"toast.session.export.failed.title": "Sitzung konnte nicht exportiert werden",
|
||||
"toast.session.export.failed.description": "Beim Exportieren der Sitzung ist ein Fehler aufgetreten",
|
||||
|
||||
"toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden",
|
||||
"toast.update.title": "Update verfügbar",
|
||||
"toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.",
|
||||
@@ -698,7 +686,6 @@ export const dict = {
|
||||
"common.moreOptions": "Weitere Optionen",
|
||||
"common.learnMore": "Mehr erfahren",
|
||||
"common.rename": "Umbenennen",
|
||||
"common.export": "Exportieren",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.archive": "Archivieren",
|
||||
"common.delete": "Löschen",
|
||||
|
||||
@@ -3,47 +3,12 @@ import {
|
||||
createDesktopNativeBundle,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_KEYS,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
|
||||
formatDesktopNativeMessage,
|
||||
parseDesktopNativeBundle,
|
||||
} from "./desktop-native"
|
||||
|
||||
describe("desktop native translations", () => {
|
||||
test("uses native language names independent of the active locale", () => {
|
||||
expect(DESKTOP_NATIVE_LOCALES.map((locale) => DESKTOP_NATIVE_LABELS[locale])).toEqual([
|
||||
"English",
|
||||
"简体中文",
|
||||
"繁體中文",
|
||||
"한국어",
|
||||
"Deutsch",
|
||||
"Español",
|
||||
"Français",
|
||||
"Dansk",
|
||||
"日本語",
|
||||
"Polski",
|
||||
"Русский",
|
||||
"Українська",
|
||||
"Bosanski",
|
||||
"العربية",
|
||||
"Norsk",
|
||||
"Português (Brasil)",
|
||||
"ไทย",
|
||||
"Türkçe",
|
||||
"हिन्दी",
|
||||
"Nederlands",
|
||||
"Bahasa Indonesia",
|
||||
"Tiếng Việt",
|
||||
"Italiano",
|
||||
"اردو",
|
||||
"پنجابی",
|
||||
"Azərbaycanca",
|
||||
"Suomi",
|
||||
"Svenska",
|
||||
])
|
||||
})
|
||||
|
||||
test("accepts the exact typed bundle", () => {
|
||||
const bundle = createDesktopNativeBundle("en", (key) => DESKTOP_NATIVE_ENGLISH[key])
|
||||
expect(parseDesktopNativeBundle(bundle)).toEqual(bundle)
|
||||
|
||||
@@ -31,37 +31,6 @@ export const DESKTOP_NATIVE_LOCALES = [
|
||||
|
||||
export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number]
|
||||
|
||||
export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
zht: "繁體中文",
|
||||
ko: "한국어",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
da: "Dansk",
|
||||
ja: "日本語",
|
||||
pl: "Polski",
|
||||
ru: "Русский",
|
||||
uk: "Українська",
|
||||
bs: "Bosanski",
|
||||
ar: "العربية",
|
||||
no: "Norsk",
|
||||
br: "Português (Brasil)",
|
||||
th: "ไทย",
|
||||
tr: "Türkçe",
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
}
|
||||
|
||||
export const DESKTOP_NATIVE_ENGLISH = {
|
||||
"desktop.menu.app": "OpenCode",
|
||||
"desktop.menu.file": "File",
|
||||
|
||||
@@ -95,8 +95,6 @@ export const dict = {
|
||||
"command.session.share.description": "Share this session and copy the URL to clipboard",
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.search.placeholder.home": "Search commands and sessions",
|
||||
@@ -480,7 +478,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Next",
|
||||
"dialog.releaseNotes.action.hideFuture": "Don't show these in the future",
|
||||
"dialog.releaseNotes.media.alt": "Release preview",
|
||||
"dialog.usageExceeded.dontShowAgain": "Don't show again",
|
||||
|
||||
"context.breakdown.title": "Context Breakdown",
|
||||
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
|
||||
@@ -492,7 +489,6 @@ export const dict = {
|
||||
|
||||
"context.systemPrompt.title": "System Prompt",
|
||||
"context.rawMessages.title": "Raw messages",
|
||||
"context.export.session": "Export session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
@@ -572,11 +568,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||
|
||||
"toast.session.export.success.title": "Session exported",
|
||||
"toast.session.export.success.description": "Saved session to {{filename}}",
|
||||
"toast.session.export.failed.title": "Failed to export session",
|
||||
"toast.session.export.failed.description": "An error occurred while exporting the session",
|
||||
|
||||
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
|
||||
|
||||
@@ -811,7 +802,6 @@ export const dict = {
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.export": "Export",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.delete": "Delete",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Dejar de compartir sesión",
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"command.session.export": "Exportar sesión",
|
||||
"command.session.export.description": "Exportar la transcripción completa de la sesión como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
@@ -570,8 +567,6 @@ export const dict = {
|
||||
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "No volver a mostrar",
|
||||
|
||||
"context.breakdown.title": "Desglose del contexto",
|
||||
"context.breakdown.note":
|
||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||
@@ -584,8 +579,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt del sistema",
|
||||
"context.rawMessages.title": "Mensajes en bruto",
|
||||
|
||||
"context.export.session": "Exportar sesión",
|
||||
|
||||
"context.stats.session": "Sesión",
|
||||
"context.stats.messages": "Mensajes",
|
||||
"context.stats.provider": "Proveedor",
|
||||
@@ -664,11 +657,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión",
|
||||
"toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión",
|
||||
|
||||
"toast.session.export.success.title": "Sesión exportada",
|
||||
"toast.session.export.success.description": "Sesión guardada en {{filename}}",
|
||||
"toast.session.export.failed.title": "No se pudo exportar la sesión",
|
||||
"toast.session.export.failed.description": "Se produjo un error al exportar la sesión",
|
||||
|
||||
"toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}",
|
||||
|
||||
"toast.update.title": "Actualización disponible",
|
||||
@@ -873,7 +861,6 @@ export const dict = {
|
||||
"common.moreOptions": "Más opciones",
|
||||
"common.learnMore": "Más información",
|
||||
"common.rename": "Renombrar",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.archive": "Archivar",
|
||||
"common.delete": "Eliminar",
|
||||
|
||||
@@ -85,9 +85,6 @@ export const dict = {
|
||||
"command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle",
|
||||
"command.session.unshare": "Peru istunnon jakaminen",
|
||||
"command.session.unshare.description": "Lopeta tämän istunnon jakaminen",
|
||||
"command.session.export": "Vie istunto",
|
||||
"command.session.export.description": "Vie istunnon koko transkriptio JSON-muodossa",
|
||||
|
||||
"palette.search.placeholder": "Hae tiedostoja, komentoja ja istuntoja",
|
||||
"palette.search.placeholder.home": "Hae komentoja ja istuntoja",
|
||||
"palette.empty": "Tuloksia ei löytynyt",
|
||||
@@ -432,8 +429,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Seuraava",
|
||||
"dialog.releaseNotes.action.hideFuture": "Älä näytä näitä jatkossa",
|
||||
"dialog.releaseNotes.media.alt": "Julkaisun esikatselu",
|
||||
"dialog.usageExceeded.dontShowAgain": "Älä näytä uudelleen",
|
||||
|
||||
"context.breakdown.title": "Kontekstin erittely",
|
||||
"context.breakdown.note":
|
||||
'Syötetokenien arvioitu jakautuminen. "Muut" sisältää työkalumäärittelyt ja muun oheissisällön.',
|
||||
@@ -444,8 +439,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Muut",
|
||||
"context.systemPrompt.title": "Järjestelmäkehote",
|
||||
"context.rawMessages.title": "Raakaviestit",
|
||||
"context.export.session": "Vie istunto",
|
||||
|
||||
"context.stats.session": "Istunto",
|
||||
"context.stats.messages": "Viestit",
|
||||
"context.stats.provider": "Palveluntarjoaja",
|
||||
@@ -512,11 +505,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!",
|
||||
"toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui",
|
||||
"toast.session.unshare.failed.description": "Virhe peruttaessa istunnon jakamista",
|
||||
"toast.session.export.success.title": "Istunto viety",
|
||||
"toast.session.export.success.description": "Istunto tallennettu tiedostoon {{filename}}",
|
||||
"toast.session.export.failed.title": "Istunnon vieminen epäonnistui",
|
||||
"toast.session.export.failed.description": "Istuntoa vietäessä tapahtui virhe",
|
||||
|
||||
"toast.session.listFailed.title": "Projektin {{project}} istuntojen lataaminen epäonnistui",
|
||||
"toast.project.reloadFailed.title": "Projektin {{project}} lataaminen uudelleen epäonnistui",
|
||||
"toast.update.title": "Päivitys saatavilla",
|
||||
@@ -734,7 +722,6 @@ export const dict = {
|
||||
"common.moreOptions": "Lisää vaihtoehtoja",
|
||||
"common.learnMore": "Lue lisää",
|
||||
"common.rename": "Nimeä uudelleen",
|
||||
"common.export": "Vie",
|
||||
"common.reset": "Palauta",
|
||||
"common.archive": "Arkistoi",
|
||||
"common.delete": "Poista",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
"command.session.export": "Exporter la session",
|
||||
"command.session.export.description": "Exporter la transcription intégrale de la session au format JSON",
|
||||
|
||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||
"palette.search.placeholder.home": "Rechercher des commandes et des sessions",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
@@ -543,8 +540,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Ne plus afficher",
|
||||
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les données annexes.",
|
||||
@@ -555,8 +550,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Autre",
|
||||
"context.systemPrompt.title": "Invite système",
|
||||
"context.rawMessages.title": "Messages bruts",
|
||||
"context.export.session": "Exporter la session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
"context.stats.provider": "Fournisseur",
|
||||
@@ -624,11 +617,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Échec de la désactivation du partage",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Une erreur s'est produite lors de la désactivation du partage de la session",
|
||||
"toast.session.export.success.title": "Session exportée",
|
||||
"toast.session.export.success.description": "Session enregistrée dans {{filename}}",
|
||||
"toast.session.export.failed.title": "Échec de l’exportation de la session",
|
||||
"toast.session.export.failed.description": "Une erreur s’est produite lors de l’exportation de la session",
|
||||
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
@@ -811,7 +799,6 @@ export const dict = {
|
||||
"common.moreOptions": "Plus d'options",
|
||||
"common.learnMore": "En savoir plus",
|
||||
"common.rename": "Renommer",
|
||||
"common.export": "Exporter",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.archive": "Archiver",
|
||||
"common.delete": "Supprimer",
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें",
|
||||
"command.session.unshare": "सेशन साझा करना बंद करें",
|
||||
"command.session.unshare.description": "इस सेशन को साझा करना बंद करें",
|
||||
"command.session.export": "सेशन निर्यात करें",
|
||||
"command.session.export.description": "सेशन की पूरी ट्रांसक्रिप्ट को JSON के रूप में निर्यात करें",
|
||||
|
||||
"palette.search.placeholder": "फ़ाइलें, कमांड और सेशन खोजें",
|
||||
"palette.search.placeholder.home": "कमांड और सेशन खोजें",
|
||||
"palette.empty": "कोई परिणाम नहीं मिला",
|
||||
@@ -546,8 +543,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "अगला",
|
||||
"dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ",
|
||||
"dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन",
|
||||
"dialog.usageExceeded.dontShowAgain": "फिर से न दिखाएँ",
|
||||
|
||||
"context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन",
|
||||
"context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।',
|
||||
"context.breakdown.system": "सिस्टम",
|
||||
@@ -557,8 +552,6 @@ export const dict = {
|
||||
"context.breakdown.other": "अन्य",
|
||||
"context.systemPrompt.title": "सिस्टम प्रॉम्प्ट",
|
||||
"context.rawMessages.title": "मूल संदेश",
|
||||
"context.export.session": "सेशन निर्यात करें",
|
||||
|
||||
"context.stats.session": "सेशन",
|
||||
"context.stats.messages": "संदेश",
|
||||
"context.stats.provider": "प्रोवाइडर",
|
||||
@@ -625,11 +618,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!",
|
||||
"toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल",
|
||||
"toast.session.unshare.failed.description": "सेशन को अनशेयर करते समय एक त्रुटि उत्पन्न हुई",
|
||||
"toast.session.export.success.title": "सेशन निर्यात किया गया",
|
||||
"toast.session.export.success.description": "सेशन को {{filename}} में सहेजा गया",
|
||||
"toast.session.export.failed.title": "सेशन निर्यात करने में विफल",
|
||||
"toast.session.export.failed.description": "सेशन निर्यात करते समय एक त्रुटि हुई",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} के लिए सेशन लोड करने में विफल",
|
||||
"toast.project.reloadFailed.title": "{{project}} को पुनः लोड करने में विफल",
|
||||
"toast.update.title": "उपलब्ध अद्यतन",
|
||||
@@ -845,7 +833,6 @@ export const dict = {
|
||||
"common.moreOptions": "अधिक विकल्प",
|
||||
"common.learnMore": "और अधिक जानें",
|
||||
"common.rename": "नाम बदलें",
|
||||
"common.export": "निर्यात करें",
|
||||
"common.reset": "रीसेट करें",
|
||||
"common.archive": "संग्रहित करें",
|
||||
"common.delete": "हटाएँ",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Hentikan berbagi",
|
||||
"command.session.unshare.description": "Hentikan berbagi sesi ini",
|
||||
|
||||
"command.session.export": "Ekspor sesi",
|
||||
"command.session.export.description": "Ekspor transkrip sesi lengkap sebagai JSON",
|
||||
|
||||
"palette.search.placeholder": "Cari berkas, perintah, dan sesi",
|
||||
"palette.search.placeholder.home": "Cari perintah dan sesi",
|
||||
"palette.empty": "Hasil tidak ditemukan",
|
||||
@@ -579,8 +576,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.hideFuture": "Jangan tampilkan ini di masa depan",
|
||||
"dialog.releaseNotes.media.alt": "Pratinjau rilis",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Jangan tampilkan lagi",
|
||||
|
||||
"context.breakdown.title": "Rincian Konteks",
|
||||
"context.breakdown.note": 'Perkiraan rincian token masukan. "Lainnya" mencakup definisi alat dan beban tambahan.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -592,8 +587,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt Sistem",
|
||||
"context.rawMessages.title": "Pesan mentah",
|
||||
|
||||
"context.export.session": "Ekspor sesi",
|
||||
|
||||
"context.stats.session": "Sesi",
|
||||
"context.stats.messages": "Pesan",
|
||||
"context.stats.provider": "Penyedia",
|
||||
@@ -671,11 +664,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Gagal menghentikan berbagi sesi",
|
||||
"toast.session.unshare.failed.description": "Terjadi kesalahan saat menghentikan berbagi sesi",
|
||||
|
||||
"toast.session.export.success.title": "Sesi diekspor",
|
||||
"toast.session.export.success.description": "Sesi disimpan ke {{filename}}",
|
||||
"toast.session.export.failed.title": "Gagal mengekspor sesi",
|
||||
"toast.session.export.failed.description": "Terjadi kesalahan saat mengekspor sesi",
|
||||
|
||||
"toast.session.listFailed.title": "Gagal memuat sesi untuk {{project}}",
|
||||
"toast.project.reloadFailed.title": "Gagal memuat ulang {{project}}",
|
||||
|
||||
@@ -909,7 +897,6 @@ export const dict = {
|
||||
"common.moreOptions": "Opsi lainnya",
|
||||
"common.learnMore": "Pelajari lebih lanjut",
|
||||
"common.rename": "Ganti nama",
|
||||
"common.export": "Ekspor",
|
||||
"common.reset": "Atur ulang",
|
||||
"common.archive": "Arsipkan",
|
||||
"common.delete": "Hapus",
|
||||
|
||||
@@ -86,9 +86,6 @@ export const dict = {
|
||||
"command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti",
|
||||
"command.session.unshare": "Annulla condivisione sessione",
|
||||
"command.session.unshare.description": "Interrompi la condivisione di questa sessione",
|
||||
"command.session.export": "Esporta sessione",
|
||||
"command.session.export.description": "Esporta la trascrizione completa della sessione in formato JSON",
|
||||
|
||||
"palette.search.placeholder": "Cerca file, comandi e sessioni",
|
||||
"palette.search.placeholder.home": "Cerca comandi e sessioni",
|
||||
"palette.empty": "Nessun risultato trovato",
|
||||
@@ -450,8 +447,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Avanti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Non mostrarli in futuro",
|
||||
"dialog.releaseNotes.media.alt": "Anteprima delle novità",
|
||||
"dialog.usageExceeded.dontShowAgain": "Non mostrare più",
|
||||
|
||||
"context.breakdown.title": "Ripartizione del contesto",
|
||||
"context.breakdown.note":
|
||||
'Ripartizione approssimativa dei token di input. "Altro" include le definizioni degli strumenti e i dati aggiuntivi.',
|
||||
@@ -462,8 +457,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Altro",
|
||||
"context.systemPrompt.title": "Prompt di sistema",
|
||||
"context.rawMessages.title": "Messaggi non elaborati",
|
||||
"context.export.session": "Esporta sessione",
|
||||
|
||||
"context.stats.session": "Sessione",
|
||||
"context.stats.messages": "Messaggi",
|
||||
"context.stats.provider": "Provider",
|
||||
@@ -531,11 +524,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Si è verificato un errore durante l'annullamento della condivisione della sessione",
|
||||
"toast.session.export.success.title": "Sessione esportata",
|
||||
"toast.session.export.success.description": "Sessione salvata in {{filename}}",
|
||||
"toast.session.export.failed.title": "Impossibile esportare la sessione",
|
||||
"toast.session.export.failed.description": "Si è verificato un errore durante l’esportazione della sessione",
|
||||
|
||||
"toast.session.listFailed.title": "Impossibile caricare le sessioni per {{project}}",
|
||||
"toast.project.reloadFailed.title": "Impossibile ricaricare {{project}}",
|
||||
"toast.update.title": "Aggiornamento disponibile",
|
||||
@@ -755,7 +743,6 @@ export const dict = {
|
||||
"common.moreOptions": "Altre opzioni",
|
||||
"common.learnMore": "Saperne di più",
|
||||
"common.rename": "Rinomina",
|
||||
"common.export": "Esporta",
|
||||
"common.reset": "Ripristina",
|
||||
"common.archive": "Archivia",
|
||||
"common.delete": "Elimina",
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
|
||||
"command.session.unshare": "セッションの共有を停止",
|
||||
"command.session.unshare.description": "このセッションの共有を停止",
|
||||
"command.session.export": "セッションをエクスポート",
|
||||
"command.session.export.description": "セッションの全記録を JSON としてエクスポート",
|
||||
|
||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||
"palette.empty": "結果が見つかりません",
|
||||
@@ -536,8 +533,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "今後表示しない",
|
||||
|
||||
"context.breakdown.title": "コンテキストの内訳",
|
||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||
"context.breakdown.system": "システム",
|
||||
@@ -547,8 +542,6 @@ export const dict = {
|
||||
"context.breakdown.other": "その他",
|
||||
"context.systemPrompt.title": "システムプロンプト",
|
||||
"context.rawMessages.title": "生のメッセージ",
|
||||
"context.export.session": "セッションをエクスポート",
|
||||
|
||||
"context.stats.session": "セッション",
|
||||
"context.stats.messages": "メッセージ",
|
||||
"context.stats.provider": "プロバイダー",
|
||||
@@ -615,11 +608,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "セッションの共有解除に成功しました!",
|
||||
"toast.session.unshare.failed.title": "セッションの共有解除に失敗しました",
|
||||
"toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました",
|
||||
"toast.session.export.success.title": "セッションをエクスポートしました",
|
||||
"toast.session.export.success.description": "セッションを {{filename}} に保存しました",
|
||||
"toast.session.export.failed.title": "セッションのエクスポートに失敗しました",
|
||||
"toast.session.export.failed.description": "セッションのエクスポート中にエラーが発生しました",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました",
|
||||
"toast.update.title": "アップデートが利用可能です",
|
||||
"toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。",
|
||||
@@ -793,7 +781,6 @@ export const dict = {
|
||||
"common.moreOptions": "その他のオプション",
|
||||
"common.learnMore": "詳細",
|
||||
"common.rename": "名前を変更",
|
||||
"common.export": "エクスポート",
|
||||
"common.reset": "リセット",
|
||||
"common.archive": "アーカイブ",
|
||||
"common.delete": "削除",
|
||||
|
||||
@@ -81,9 +81,6 @@ export const dict = {
|
||||
"command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사",
|
||||
"command.session.unshare": "세션 공유 중지",
|
||||
"command.session.unshare.description": "이 세션 공유 중지",
|
||||
"command.session.export": "세션 내보내기",
|
||||
"command.session.export.description": "전체 세션 기록을 JSON으로 내보내기",
|
||||
|
||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||
"palette.empty": "결과 없음",
|
||||
@@ -339,8 +336,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
|
||||
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "예: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "다시 표시하지 않기",
|
||||
|
||||
"context.breakdown.title": "컨텍스트 분석",
|
||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||
"context.breakdown.system": "시스템",
|
||||
@@ -350,8 +345,6 @@ export const dict = {
|
||||
"context.breakdown.other": "기타",
|
||||
"context.systemPrompt.title": "시스템 프롬프트",
|
||||
"context.rawMessages.title": "원시 메시지",
|
||||
"context.export.session": "세션 내보내기",
|
||||
|
||||
"context.stats.session": "세션",
|
||||
"context.stats.messages": "메시지",
|
||||
"context.stats.provider": "공급자",
|
||||
@@ -417,11 +410,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!",
|
||||
"toast.session.unshare.failed.title": "세션 공유 해제 실패",
|
||||
"toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다",
|
||||
"toast.session.export.success.title": "세션을 내보냈습니다",
|
||||
"toast.session.export.success.description": "세션 저장 위치: {{filename}}",
|
||||
"toast.session.export.failed.title": "세션 내보내기 실패",
|
||||
"toast.session.export.failed.description": "세션을 내보내는 동안 오류가 발생했습니다",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다",
|
||||
"toast.update.title": "업데이트 가능",
|
||||
"toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.",
|
||||
@@ -556,7 +544,6 @@ export const dict = {
|
||||
"common.moreOptions": "더 많은 옵션",
|
||||
"common.learnMore": "더 알아보기",
|
||||
"common.rename": "이름 바꾸기",
|
||||
"common.export": "내보내기",
|
||||
"common.reset": "초기화",
|
||||
"common.archive": "보관",
|
||||
"common.delete": "삭제",
|
||||
|
||||
@@ -178,9 +178,6 @@ export const dict = {
|
||||
"command.session.share.description": "Deel deze sessie en kopieer de URL naar het klembord",
|
||||
"command.session.unshare": "Sessie niet meer delen",
|
||||
"command.session.unshare.description": "Stop met het delen van deze sessie",
|
||||
"command.session.export": "Sessie exporteren",
|
||||
"command.session.export.description": "Het volledige sessietranscript exporteren als JSON",
|
||||
|
||||
"palette.search.placeholder": "Zoek naar bestanden, opdrachten en sessies",
|
||||
"palette.search.placeholder.home": "Opdrachten en sessies zoeken",
|
||||
"palette.empty": "Geen resultaten gevonden",
|
||||
@@ -542,8 +539,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Volgende",
|
||||
"dialog.releaseNotes.action.hideFuture": "Laat deze in de toekomst niet zien",
|
||||
"dialog.releaseNotes.media.alt": "Releasevoorbeeld",
|
||||
"dialog.usageExceeded.dontShowAgain": "Niet meer weergeven",
|
||||
|
||||
"context.breakdown.title": "Contextanalyse",
|
||||
"context.breakdown.note":
|
||||
'Geschatte uitsplitsing van invoertokens. "Overig" omvat gereedschapsdefinities en overhead.',
|
||||
@@ -554,8 +549,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Overig",
|
||||
"context.systemPrompt.title": "Systeemprompt",
|
||||
"context.rawMessages.title": "Ruwe berichten",
|
||||
"context.export.session": "Sessie exporteren",
|
||||
|
||||
"context.stats.session": "Sessie",
|
||||
"context.stats.messages": "Berichten",
|
||||
"context.stats.provider": "Aanbieder",
|
||||
@@ -623,11 +616,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kan het delen van de sessie niet ongedaan maken",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Er is een fout opgetreden bij het ongedaan maken van het delen van de sessie",
|
||||
"toast.session.export.success.title": "Sessie geëxporteerd",
|
||||
"toast.session.export.success.description": "Sessie opgeslagen in {{filename}}",
|
||||
"toast.session.export.failed.title": "Kan sessie niet exporteren",
|
||||
"toast.session.export.failed.description": "Er is een fout opgetreden tijdens het exporteren van de sessie",
|
||||
|
||||
"toast.session.listFailed.title": "Kan sessies voor {{project}} niet laden",
|
||||
"toast.project.reloadFailed.title": "Kan {{project}} niet opnieuw laden",
|
||||
"toast.update.title": "Update beschikbaar",
|
||||
@@ -844,7 +832,6 @@ export const dict = {
|
||||
"common.moreOptions": "Meer opties",
|
||||
"common.learnMore": "Meer informatie",
|
||||
"common.rename": "Hernoemen",
|
||||
"common.export": "Exporteren",
|
||||
"common.reset": "Opnieuw instellen",
|
||||
"common.archive": "Archiveren",
|
||||
"common.delete": "Verwijderen",
|
||||
|
||||
@@ -191,9 +191,6 @@ export const dict = {
|
||||
"command.session.unshare": "Slutt å dele sesjon",
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"command.session.export": "Eksporter sesjon",
|
||||
"command.session.export.description": "Eksporter hele sesjonsutskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
@@ -473,8 +470,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Ikke vis igjen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling av inndatatokener. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||
@@ -487,8 +482,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå meldinger",
|
||||
|
||||
"context.export.session": "Eksporter sesjon",
|
||||
|
||||
"context.stats.session": "Sesjon",
|
||||
"context.stats.messages": "Meldinger",
|
||||
"context.stats.provider": "Leverandør",
|
||||
@@ -566,11 +559,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
|
||||
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
|
||||
|
||||
"toast.session.export.success.title": "Sesjon eksportert",
|
||||
"toast.session.export.success.description": "Sesjonen ble lagret i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere sesjon",
|
||||
"toast.session.export.failed.description": "Det oppstod en feil under eksport av sesjonen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
|
||||
|
||||
"toast.update.title": "Oppdatering tilgjengelig",
|
||||
@@ -733,7 +721,6 @@ export const dict = {
|
||||
"common.moreOptions": "Flere alternativer",
|
||||
"common.learnMore": "Lær mer",
|
||||
"common.rename": "Gi nytt navn",
|
||||
"common.export": "Eksporter",
|
||||
"common.reset": "Tilbakestill",
|
||||
"common.archive": "Arkiver",
|
||||
"common.delete": "Slett",
|
||||
|
||||
@@ -184,9 +184,6 @@ export const dict = {
|
||||
"command.session.share.description": "اس سیشن نو شیئر کرو تے URL نو کلپ بورڈ تے کاپی کرو",
|
||||
"command.session.unshare": "سیشن شیئر کرنا بند کرو",
|
||||
"command.session.unshare.description": "اس سیشن نو شیئر کرنا بند کرو",
|
||||
"command.session.export": "سیشن برآمد کرو",
|
||||
"command.session.export.description": "سیشن دی پوری نقل JSON دی شکل وچ برآمد کرو",
|
||||
|
||||
"palette.search.placeholder": "فائلاں، کمانڈز، تے سیشنز تلاش کرو",
|
||||
"palette.search.placeholder.home": "سرچ کمانڈز تے سیشنز",
|
||||
"palette.empty": "کوئی نتیجہ نئیں ملیا",
|
||||
@@ -547,8 +544,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "اگلا",
|
||||
"dialog.releaseNotes.action.hideFuture": "ایہہ اگوں نہ وکھاؤ",
|
||||
"dialog.releaseNotes.media.alt": "ریلیز پیش نظارہ",
|
||||
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ وکھاؤ",
|
||||
|
||||
"context.breakdown.title": "کانٹیکسٹ دی ونڈ",
|
||||
"context.breakdown.note": 'ان پٹ ٹوکناں دی لگ بھگ ونڈ۔ "ہور" وچ ٹول دیاں تعریفاں تے وادھو خرچ شامل نیں۔',
|
||||
"context.breakdown.system": "نظام",
|
||||
@@ -558,8 +553,6 @@ export const dict = {
|
||||
"context.breakdown.other": "ہور",
|
||||
"context.systemPrompt.title": "سسٹم پرامپٹ",
|
||||
"context.rawMessages.title": "خام سنیہے",
|
||||
"context.export.session": "سیشن برآمد کرو",
|
||||
|
||||
"context.stats.session": "سیشن",
|
||||
"context.stats.messages": "سنیہے",
|
||||
"context.stats.provider": "پرووائیڈر",
|
||||
@@ -626,11 +619,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "سیشن دا شیئر کامیابی نال بند ہو گیا!",
|
||||
"toast.session.unshare.failed.title": "سیشن ان شیئر کرن چ ناکام رہیا",
|
||||
"toast.session.unshare.failed.description": "سیشن ان شیئر کردے ویلے کوئی غلطی ہو گئی",
|
||||
"toast.session.export.success.title": "سیشن برآمد ہو گیا",
|
||||
"toast.session.export.success.description": "سیشن نوں \u2068{{filename}}\u2069 وچ محفوظ کر دتا گیا",
|
||||
"toast.session.export.failed.title": "سیشن برآمد کرن چ ناکامی ہوئی",
|
||||
"toast.session.export.failed.description": "سیشن برآمد کردے ویلے اک غلطی ہو گئی",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} لئی سیشن لوڈ کرن چ ناکام رہیا",
|
||||
"toast.project.reloadFailed.title": "{{project}} دوبارہ لوڈ نئیں ہو سکیا",
|
||||
"toast.update.title": "اپ ڈیٹ دستیاب اے",
|
||||
@@ -843,7 +831,6 @@ export const dict = {
|
||||
"common.moreOptions": "ہور اختیارات",
|
||||
"common.learnMore": "ہور جانو",
|
||||
"common.rename": "ناں بدلو",
|
||||
"common.export": "برآمد کرو",
|
||||
"common.reset": "ری سیٹ کرو",
|
||||
"common.archive": "آرکائیو کرو",
|
||||
"common.delete": "مکاؤ",
|
||||
|
||||
@@ -63,7 +63,7 @@ const domains = [
|
||||
},
|
||||
] as const
|
||||
|
||||
describe("i18n parity", () => {
|
||||
describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
||||
test("non-English locales have every English key and required plural variants", async () => {
|
||||
for (const domain of domains) {
|
||||
const source = await dictionary(domain.source)
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "Udostępnij tę sesję i skopiuj adres URL do schowka",
|
||||
"command.session.unshare": "Przestań udostępniać sesję",
|
||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||
"command.session.export": "Eksportuj sesję",
|
||||
"command.session.export.description": "Eksportuj pełny zapis sesji w formacie JSON",
|
||||
|
||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||
"palette.empty": "Brak wyników",
|
||||
@@ -538,8 +535,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "np. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nie pokazuj ponownie",
|
||||
|
||||
"context.breakdown.title": "Podział kontekstu",
|
||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||
"context.breakdown.system": "System",
|
||||
@@ -549,8 +544,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Inne",
|
||||
"context.systemPrompt.title": "Prompt systemowy",
|
||||
"context.rawMessages.title": "Surowe wiadomości",
|
||||
"context.export.session": "Eksportuj sesję",
|
||||
|
||||
"context.stats.session": "Sesja",
|
||||
"context.stats.messages": "Wiadomości",
|
||||
"context.stats.provider": "Dostawca",
|
||||
@@ -617,11 +610,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!",
|
||||
"toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji",
|
||||
"toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji",
|
||||
"toast.session.export.success.title": "Sesja wyeksportowana",
|
||||
"toast.session.export.success.description": "Sesję zapisano jako {{filename}}",
|
||||
"toast.session.export.failed.title": "Nie udało się wyeksportować sesji",
|
||||
"toast.session.export.failed.description": "Wystąpił błąd podczas eksportowania sesji",
|
||||
|
||||
"toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}",
|
||||
"toast.update.title": "Dostępna aktualizacja",
|
||||
"toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.",
|
||||
@@ -809,7 +797,6 @@ export const dict = {
|
||||
"common.moreOptions": "Więcej opcji",
|
||||
"common.learnMore": "Dowiedz się więcej",
|
||||
"common.rename": "Zmień nazwę",
|
||||
"common.export": "Eksportuj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.archive": "Archiwizuj",
|
||||
"common.delete": "Usuń",
|
||||
|
||||
@@ -192,9 +192,6 @@ export const dict = {
|
||||
"command.session.unshare": "Отменить публикацию",
|
||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||
|
||||
"command.session.export": "Экспортировать сессию",
|
||||
"command.session.export.description": "Экспортировать полную историю сессии в формате JSON",
|
||||
|
||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||
"palette.empty": "Ничего не найдено",
|
||||
@@ -567,8 +564,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Запускается после создания нового рабочего пространства (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "например, bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Больше не показывать",
|
||||
|
||||
"context.breakdown.title": "Разбивка контекста",
|
||||
"context.breakdown.note":
|
||||
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
||||
@@ -581,8 +576,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Системный промпт",
|
||||
"context.rawMessages.title": "Исходные сообщения",
|
||||
|
||||
"context.export.session": "Экспортировать сессию",
|
||||
|
||||
"context.stats.session": "Сессия",
|
||||
"context.stats.messages": "Сообщения",
|
||||
"context.stats.provider": "Провайдер",
|
||||
@@ -660,11 +653,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Не удалось отменить публикацию",
|
||||
"toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации",
|
||||
|
||||
"toast.session.export.success.title": "Сессия экспортирована",
|
||||
"toast.session.export.success.description": "Сессия сохранена в файл {{filename}}",
|
||||
"toast.session.export.failed.title": "Не удалось экспортировать сессию",
|
||||
"toast.session.export.failed.description": "Произошла ошибка при экспорте сессии",
|
||||
|
||||
"toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}",
|
||||
|
||||
"toast.update.title": "Доступно обновление",
|
||||
@@ -870,7 +858,6 @@ export const dict = {
|
||||
"common.moreOptions": "Дополнительные опции",
|
||||
"common.learnMore": "Подробнее",
|
||||
"common.rename": "Переименовать",
|
||||
"common.export": "Экспортировать",
|
||||
"common.reset": "Сбросить",
|
||||
"common.archive": "Архивировать",
|
||||
"common.delete": "Удалить",
|
||||
|
||||
@@ -179,9 +179,6 @@ export const dict = {
|
||||
"command.session.share.description": "Dela den här sessionen och kopiera URL:en till urklipp",
|
||||
"command.session.unshare": "Sluta dela session",
|
||||
"command.session.unshare.description": "Sluta dela den här sessionen",
|
||||
"command.session.export": "Exportera session",
|
||||
"command.session.export.description": "Exportera hela sessionsutskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Sök efter filer, kommandon och sessioner",
|
||||
"palette.search.placeholder.home": "Sök bland kommandon och sessioner",
|
||||
"palette.empty": "Inga resultat hittades",
|
||||
@@ -541,8 +538,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Nästa",
|
||||
"dialog.releaseNotes.action.hideFuture": "Visa inte dessa i framtiden",
|
||||
"dialog.releaseNotes.media.alt": "Förhandsvisning av version",
|
||||
"dialog.usageExceeded.dontShowAgain": "Visa inte igen",
|
||||
|
||||
"context.breakdown.title": "Kontextfördelning",
|
||||
"context.breakdown.note":
|
||||
'Ungefärlig uppdelning av inmatningstokens. "Övrigt" inkluderar verktygsdefinitioner och overhead.',
|
||||
@@ -553,8 +548,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Övrigt",
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Råmeddelanden",
|
||||
"context.export.session": "Exportera session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Meddelanden",
|
||||
"context.stats.provider": "Leverantör",
|
||||
@@ -621,11 +614,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Delningen av sessionen har avslutats!",
|
||||
"toast.session.unshare.failed.title": "Det gick inte att avsluta delningen",
|
||||
"toast.session.unshare.failed.description": "Ett fel uppstod när delningen skulle avslutas",
|
||||
"toast.session.export.success.title": "Session exporterad",
|
||||
"toast.session.export.success.description": "Sessionen sparades i {{filename}}",
|
||||
"toast.session.export.failed.title": "Det gick inte att exportera sessionen",
|
||||
"toast.session.export.failed.description": "Ett fel uppstod när sessionen exporterades",
|
||||
|
||||
"toast.session.listFailed.title": "Det gick inte att läsa in sessioner för {{project}}",
|
||||
"toast.project.reloadFailed.title": "Det gick inte att ladda om {{project}}",
|
||||
"toast.update.title": "Uppdatering tillgänglig",
|
||||
@@ -840,7 +828,6 @@ export const dict = {
|
||||
"common.moreOptions": "Fler alternativ",
|
||||
"common.learnMore": "Läs mer",
|
||||
"common.rename": "Byt namn",
|
||||
"common.export": "Exportera",
|
||||
"common.reset": "Återställ",
|
||||
"common.archive": "Arkivera",
|
||||
"common.delete": "Radera",
|
||||
|
||||
@@ -191,9 +191,6 @@ export const dict = {
|
||||
"command.session.unshare": "ยกเลิกการแชร์เซสชัน",
|
||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||
|
||||
"command.session.export": "ส่งออกเซสชัน",
|
||||
"command.session.export.description": "ส่งออกบันทึกทั้งหมดของเซสชันเป็น JSON",
|
||||
|
||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
|
||||
"palette.empty": "ไม่พบผลลัพธ์",
|
||||
@@ -564,8 +561,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "เช่น bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "ไม่ต้องแสดงอีก",
|
||||
|
||||
"context.breakdown.title": "การแบ่งบริบท",
|
||||
"context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด',
|
||||
"context.breakdown.system": "ระบบ",
|
||||
@@ -577,8 +572,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "พรอมต์ระบบ",
|
||||
"context.rawMessages.title": "ข้อความดิบ",
|
||||
|
||||
"context.export.session": "ส่งออกเซสชัน",
|
||||
|
||||
"context.stats.session": "เซสชัน",
|
||||
"context.stats.messages": "ข้อความ",
|
||||
"context.stats.provider": "ผู้ให้บริการ",
|
||||
@@ -657,11 +650,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน",
|
||||
"toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน",
|
||||
|
||||
"toast.session.export.success.title": "ส่งออกเซสชันแล้ว",
|
||||
"toast.session.export.success.description": "บันทึกเซสชันไปยัง {{filename}} แล้ว",
|
||||
"toast.session.export.failed.title": "ไม่สามารถส่งออกเซสชัน",
|
||||
"toast.session.export.failed.description": "เกิดข้อผิดพลาดขณะส่งออกเซสชัน",
|
||||
|
||||
"toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}",
|
||||
|
||||
"toast.update.title": "มีการอัปเดต",
|
||||
@@ -855,7 +843,6 @@ export const dict = {
|
||||
"common.moreOptions": "ตัวเลือกเพิ่มเติม",
|
||||
"common.learnMore": "เรียนรู้เพิ่มเติม",
|
||||
"common.rename": "เปลี่ยนชื่อ",
|
||||
"common.export": "ส่งออก",
|
||||
"common.reset": "รีเซ็ต",
|
||||
"common.archive": "จัดเก็บ",
|
||||
"common.delete": "ลบ",
|
||||
|
||||
@@ -197,9 +197,6 @@ export const dict = {
|
||||
"command.session.unshare": "Paylaşımı kaldır",
|
||||
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
||||
|
||||
"command.session.export": "Oturumu dışa aktar",
|
||||
"command.session.export.description": "Oturumun tam dökümünü JSON olarak dışa aktar",
|
||||
|
||||
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
||||
"palette.search.placeholder.home": "Komut ve oturum ara",
|
||||
"palette.empty": "Sonuç bulunamadı",
|
||||
@@ -572,8 +569,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "örneğin bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Bir daha gösterme",
|
||||
|
||||
"context.breakdown.title": "Bağlam Dökümü",
|
||||
"context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -585,8 +580,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Sistem İstemi",
|
||||
"context.rawMessages.title": "Ham mesajlar",
|
||||
|
||||
"context.export.session": "Oturumu dışa aktar",
|
||||
|
||||
"context.stats.session": "Oturum",
|
||||
"context.stats.messages": "Mesajlar",
|
||||
"context.stats.provider": "Sağlayıcı",
|
||||
@@ -665,11 +658,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı",
|
||||
"toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu",
|
||||
|
||||
"toast.session.export.success.title": "Oturum dışa aktarıldı",
|
||||
"toast.session.export.success.description": "Oturum {{filename}} dosyasına kaydedildi",
|
||||
"toast.session.export.failed.title": "Oturum dışa aktarılamadı",
|
||||
"toast.session.export.failed.description": "Oturum dışa aktarılırken bir hata oluştu",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi",
|
||||
|
||||
"toast.update.title": "Güncelleme mevcut",
|
||||
@@ -873,7 +861,6 @@ export const dict = {
|
||||
"common.moreOptions": "Daha fazla seçenek",
|
||||
"common.learnMore": "Daha fazla bilgi",
|
||||
"common.rename": "Yeniden adlandır",
|
||||
"common.export": "Dışa aktar",
|
||||
"common.reset": "Sıfırla",
|
||||
"common.archive": "Arşivle",
|
||||
"common.delete": "Sil",
|
||||
|
||||
@@ -193,9 +193,6 @@ export const dict = {
|
||||
"command.session.unshare": "Припинити поширення сесії",
|
||||
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
||||
|
||||
"command.session.export": "Експортувати сесію",
|
||||
"command.session.export.description": "Експортувати повну історію сесії у форматі JSON",
|
||||
|
||||
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
||||
"palette.search.placeholder.home": "Пошук команд і сесій",
|
||||
"palette.empty": "Результатів не знайдено",
|
||||
@@ -581,8 +578,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому",
|
||||
"dialog.releaseNotes.media.alt": "Попередній перегляд релізу",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Більше не показувати",
|
||||
|
||||
"context.breakdown.title": "Розподіл контексту",
|
||||
"context.breakdown.note":
|
||||
'Приблизний розподіл вхідних токенів. "Інше" включає визначення інструментів і накладні витрати.',
|
||||
@@ -595,8 +590,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Системний запит",
|
||||
"context.rawMessages.title": "Необроблені повідомлення",
|
||||
|
||||
"context.export.session": "Експортувати сесію",
|
||||
|
||||
"context.stats.session": "Сесія",
|
||||
"context.stats.messages": "Повідомлення",
|
||||
"context.stats.provider": "Провайдер",
|
||||
@@ -675,11 +668,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Не вдалося припинити поширення сесії",
|
||||
"toast.session.unshare.failed.description": "Під час припинення поширення сесії сталася помилка",
|
||||
|
||||
"toast.session.export.success.title": "Сесію експортовано",
|
||||
"toast.session.export.success.description": "Сесію збережено у файл {{filename}}",
|
||||
"toast.session.export.failed.title": "Не вдалося експортувати сесію",
|
||||
"toast.session.export.failed.description": "Під час експортування сесії сталася помилка",
|
||||
|
||||
"toast.session.listFailed.title": "Не вдалося завантажити сесії для {{project}}",
|
||||
"toast.project.reloadFailed.title": "Не вдалося перезавантажити {{project}}",
|
||||
|
||||
@@ -922,7 +910,6 @@ export const dict = {
|
||||
"common.moreOptions": "Більше параметрів",
|
||||
"common.learnMore": "Дізнатися більше",
|
||||
"common.rename": "Перейменувати",
|
||||
"common.export": "Експортувати",
|
||||
"common.reset": "Скинути",
|
||||
"common.archive": "Архівувати",
|
||||
"common.delete": "Видалити",
|
||||
|
||||
@@ -186,9 +186,6 @@ export const dict = {
|
||||
"command.session.share.description": "اس سیشن کا اشتراک کریں اور URL کو کلپ بورڈ میں کاپی کریں۔",
|
||||
"command.session.unshare": "سیشن کا اشتراک ختم کریں۔",
|
||||
"command.session.unshare.description": "اس سیشن کا اشتراک کرنا بند کریں۔",
|
||||
"command.session.export": "سیشن برآمد کریں",
|
||||
"command.session.export.description": "سیشن کی مکمل نقل JSON کی صورت میں برآمد کریں",
|
||||
|
||||
"palette.search.placeholder": "فائلیں، کمانڈز اور سیشنز تلاش کریں۔",
|
||||
"palette.search.placeholder.home": "کمانڈز اور سیشن تلاش کریں۔",
|
||||
"palette.empty": "کوئی نتیجہ نہیں ملا",
|
||||
@@ -549,8 +546,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "اگلا",
|
||||
"dialog.releaseNotes.action.hideFuture": "مستقبل میں یہ نہ دکھائیں۔",
|
||||
"dialog.releaseNotes.media.alt": "ریلیز کا پیش منظر",
|
||||
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ دکھائیں",
|
||||
|
||||
"context.breakdown.title": "سیاق و سباق کی تفصیل",
|
||||
"context.breakdown.note": 'ان پٹ ٹوکنز کی تخمینی تقسیم۔ "دیگر" میں ٹول کی تعریفیں اور اضافی بوجھ شامل ہیں۔',
|
||||
"context.breakdown.system": "سسٹم",
|
||||
@@ -560,8 +555,6 @@ export const dict = {
|
||||
"context.breakdown.other": "دیگر",
|
||||
"context.systemPrompt.title": "سسٹم پرامپٹ",
|
||||
"context.rawMessages.title": "خام پیغامات",
|
||||
"context.export.session": "سیشن برآمد کریں",
|
||||
|
||||
"context.stats.session": "سیشن",
|
||||
"context.stats.messages": "پیغامات",
|
||||
"context.stats.provider": "فراہم کنندہ",
|
||||
@@ -628,11 +621,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "سیشن کا اشتراک کامیابی سے ختم ہو گیا!",
|
||||
"toast.session.unshare.failed.title": "سیشن کا اشتراک ختم کرنے میں ناکام",
|
||||
"toast.session.unshare.failed.description": "سیشن کا اشتراک ختم کرتے وقت ایک خرابی پیش آگئی",
|
||||
"toast.session.export.success.title": "سیشن برآمد کر دیا گیا",
|
||||
"toast.session.export.success.description": "سیشن کو \u2068{{filename}}\u2069 میں محفوظ کر دیا گیا",
|
||||
"toast.session.export.failed.title": "سیشن برآمد کرنے میں ناکام",
|
||||
"toast.session.export.failed.description": "سیشن برآمد کرتے وقت ایک خرابی پیش آ گئی",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} کے لیے سیشن لوڈ کرنے میں ناکام",
|
||||
"toast.project.reloadFailed.title": "{{project}} کو دوبارہ لوڈ کرنے میں ناکام",
|
||||
"toast.update.title": "اپ ڈیٹ دستیاب ہے۔",
|
||||
@@ -846,7 +834,6 @@ export const dict = {
|
||||
"common.moreOptions": "مزید اختیارات",
|
||||
"common.learnMore": "مزید جانیں",
|
||||
"common.rename": "نام تبدیل کریں۔",
|
||||
"common.export": "برآمد کریں",
|
||||
"common.reset": "دوبارہ ترتیب دیں۔",
|
||||
"common.archive": "آرکائیو کریں",
|
||||
"common.delete": "حذف کریں۔",
|
||||
|
||||
@@ -185,9 +185,6 @@ export const dict = {
|
||||
"command.session.share.description": "Chia sẻ phiên này và sao chép URL vào bảng tạm",
|
||||
"command.session.unshare": "Ngừng chia sẻ phiên",
|
||||
"command.session.unshare.description": "Dừng chia sẻ phiên này",
|
||||
"command.session.export": "Xuất phiên",
|
||||
"command.session.export.description": "Xuất toàn bộ bản ghi phiên dưới dạng JSON",
|
||||
|
||||
"palette.search.placeholder": "Tìm kiếm tệp, lệnh và phiên",
|
||||
"palette.search.placeholder.home": "Tìm kiếm lệnh và phiên",
|
||||
"palette.empty": "Không tìm thấy kết quả nào",
|
||||
@@ -546,8 +543,6 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Tiếp theo",
|
||||
"dialog.releaseNotes.action.hideFuture": "Không hiển thị lại",
|
||||
"dialog.releaseNotes.media.alt": "Xem trước bản phát hành",
|
||||
"dialog.usageExceeded.dontShowAgain": "Không hiển thị lại",
|
||||
|
||||
"context.breakdown.title": "Phân tích ngữ cảnh",
|
||||
"context.breakdown.note":
|
||||
'Phân tích gần đúng của token đầu vào. "Khác" bao gồm các định nghĩa công cụ và chi phí chung.',
|
||||
@@ -558,8 +553,6 @@ export const dict = {
|
||||
"context.breakdown.other": "Khác",
|
||||
"context.systemPrompt.title": "Lời nhắc hệ thống",
|
||||
"context.rawMessages.title": "Tin nhắn thô",
|
||||
"context.export.session": "Xuất phiên",
|
||||
|
||||
"context.stats.session": "Phiên",
|
||||
"context.stats.messages": "Tin nhắn",
|
||||
"context.stats.provider": "Nhà cung cấp",
|
||||
@@ -626,11 +619,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Đã hủy chia sẻ phiên thành công!",
|
||||
"toast.session.unshare.failed.title": "Không hủy chia sẻ được phiên",
|
||||
"toast.session.unshare.failed.description": "Đã xảy ra lỗi khi hủy chia sẻ phiên",
|
||||
"toast.session.export.success.title": "Đã xuất phiên",
|
||||
"toast.session.export.success.description": "Đã lưu phiên vào {{filename}}",
|
||||
"toast.session.export.failed.title": "Không thể xuất phiên",
|
||||
"toast.session.export.failed.description": "Đã xảy ra lỗi khi xuất phiên",
|
||||
|
||||
"toast.session.listFailed.title": "Không thể tải phiên cho {{project}}",
|
||||
"toast.project.reloadFailed.title": "Không thể tải lại {{project}}",
|
||||
"toast.update.title": "Đã có bản cập nhật",
|
||||
@@ -846,7 +834,6 @@ export const dict = {
|
||||
"common.moreOptions": "Nhiều lựa chọn hơn",
|
||||
"common.learnMore": "Tìm hiểu thêm",
|
||||
"common.rename": "Đổi tên",
|
||||
"common.export": "Xuất",
|
||||
"common.reset": "Đặt lại",
|
||||
"common.archive": "Lưu trữ",
|
||||
"common.delete": "Xóa",
|
||||
|
||||
@@ -218,9 +218,6 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享会话",
|
||||
"command.session.unshare.description": "停止分享此会话",
|
||||
|
||||
"command.session.export": "导出会话",
|
||||
"command.session.export.description": "将完整会话记录导出为 JSON",
|
||||
|
||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||
"palette.search.placeholder.home": "搜索命令和会话",
|
||||
"palette.empty": "未找到结果",
|
||||
@@ -580,8 +577,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "创建新工作区 (worktree) 后运行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "不再显示",
|
||||
|
||||
"context.breakdown.title": "上下文细分",
|
||||
"context.breakdown.note": "输入令牌的大致细分。“其他”包含工具定义和开销。",
|
||||
"context.breakdown.system": "系统",
|
||||
@@ -591,8 +586,6 @@ export const dict = {
|
||||
"context.breakdown.other": "其他",
|
||||
"context.systemPrompt.title": "系统提示词",
|
||||
"context.rawMessages.title": "原始消息",
|
||||
"context.export.session": "导出会话",
|
||||
|
||||
"context.stats.session": "会话",
|
||||
"context.stats.messages": "消息数",
|
||||
"context.stats.provider": "提供商",
|
||||
@@ -661,11 +654,6 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "会话已成功取消分享",
|
||||
"toast.session.unshare.failed.title": "取消分享失败",
|
||||
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
|
||||
"toast.session.export.success.title": "会话已导出",
|
||||
"toast.session.export.success.description": "已将会话保存到 {{filename}}",
|
||||
"toast.session.export.failed.title": "导出会话失败",
|
||||
"toast.session.export.failed.description": "导出会话时发生错误",
|
||||
|
||||
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
|
||||
"toast.update.title": "有可用更新",
|
||||
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
|
||||
@@ -851,7 +839,6 @@ export const dict = {
|
||||
"common.moreOptions": "更多选项",
|
||||
"common.learnMore": "了解更多",
|
||||
"common.rename": "重命名",
|
||||
"common.export": "导出",
|
||||
"common.reset": "重置",
|
||||
"common.archive": "归档",
|
||||
"common.delete": "删除",
|
||||
|
||||
@@ -195,9 +195,6 @@ export const dict = {
|
||||
"command.session.unshare": "取消分享工作階段",
|
||||
"command.session.unshare.description": "停止分享此工作階段",
|
||||
|
||||
"command.session.export": "匯出工作階段",
|
||||
"command.session.export.description": "將完整的工作階段記錄匯出為 JSON",
|
||||
|
||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||
"palette.search.placeholder.home": "搜尋命令和工作階段",
|
||||
"palette.empty": "找不到結果",
|
||||
@@ -561,8 +558,6 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "工作區啟動腳本",
|
||||
"dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例如 bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "不再顯示",
|
||||
|
||||
"context.breakdown.title": "上下文細分",
|
||||
"context.breakdown.note": "輸入 token 的概略細分。「其他」包含工具定義和額外負擔。",
|
||||
"context.breakdown.system": "系統",
|
||||
@@ -574,8 +569,6 @@ export const dict = {
|
||||
"context.systemPrompt.title": "系統提示詞",
|
||||
"context.rawMessages.title": "原始訊息",
|
||||
|
||||
"context.export.session": "匯出工作階段",
|
||||
|
||||
"context.stats.session": "工作階段",
|
||||
"context.stats.messages": "訊息數",
|
||||
"context.stats.provider": "提供者",
|
||||
@@ -653,11 +646,6 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "取消分享失敗",
|
||||
"toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤",
|
||||
|
||||
"toast.session.export.success.title": "工作階段已匯出",
|
||||
"toast.session.export.success.description": "已將工作階段儲存至 {{filename}}",
|
||||
"toast.session.export.failed.title": "匯出工作階段失敗",
|
||||
"toast.session.export.failed.description": "匯出工作階段時發生錯誤",
|
||||
|
||||
"toast.session.listFailed.title": "無法載入 {{project}} 的工作階段",
|
||||
|
||||
"toast.update.title": "有可用更新",
|
||||
@@ -848,7 +836,6 @@ export const dict = {
|
||||
"common.moreOptions": "更多選項",
|
||||
"common.learnMore": "深入了解",
|
||||
"common.rename": "重新命名",
|
||||
"common.export": "匯出",
|
||||
"common.reset": "重設",
|
||||
"common.archive": "封存",
|
||||
"common.delete": "刪除",
|
||||
|
||||
@@ -53,7 +53,6 @@ import type {
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import { normalize } from "@opencode-ai/session-ui/session-diff"
|
||||
@@ -807,29 +806,6 @@ export function MessageTimeline(props: {
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
|
||||
const exportSession = async (sessionID: string) => {
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
const session = sync().session.get(sessionID)
|
||||
if (!session) return
|
||||
@@ -1588,9 +1564,6 @@ export function MessageTimeline(props: {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => exportSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.export")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -1662,9 +1635,6 @@ export function MessageTimeline(props: {
|
||||
{language.t("session.share.action.share")}...
|
||||
</MenuV2.Item>
|
||||
</Show>
|
||||
<MenuV2.Item onSelect={() => exportSession(id)}>
|
||||
{language.t("common.export")}...
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => void archiveSession(id)}>
|
||||
{language.t("common.archive")}
|
||||
</MenuV2.Item>
|
||||
|
||||
@@ -12,11 +12,10 @@ import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { Message, Part, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionOwnership } from "./session-ownership"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -232,31 +231,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
)
|
||||
}
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openFile = () => {
|
||||
void openDialog(
|
||||
() => import("@/components/dialog-select-file"),
|
||||
@@ -484,14 +458,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: fork,
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.export",
|
||||
title: language.t("command.session.export"),
|
||||
description: language.t("command.session.export.description"),
|
||||
slash: "export",
|
||||
disabled: !params.id,
|
||||
onSelect: exportSession,
|
||||
}),
|
||||
]
|
||||
|
||||
const fileCmds = () => {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fetchSessionExport, sessionExportFilename } from "./session-export"
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
describe("sessionExportFilename", () => {
|
||||
test("generates filename from title", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", title: "Clone PR in worktree from fork" })).toBe(
|
||||
"clone-pr-in-worktree-from-fork.json",
|
||||
)
|
||||
})
|
||||
|
||||
test("generates filename from slug when title missing", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", slug: "my-session-slug" })).toBe("my-session-slug.json")
|
||||
})
|
||||
|
||||
test("falls back to id when title and slug are empty", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123" })).toBe("ses_123.json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchSessionExport", () => {
|
||||
test("fetches full transcript from client", async () => {
|
||||
const session = { id: "ses_1", title: "Test Session" } as Session
|
||||
const msg = { id: "msg_1", role: "user" } as Message
|
||||
const part = { id: "prt_1", type: "text", text: "hello" } as Part
|
||||
const messages = [{ info: msg, parts: [part] }]
|
||||
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: session }),
|
||||
messages: async () => ({ data: messages }),
|
||||
},
|
||||
}
|
||||
|
||||
const result = await fetchSessionExport({
|
||||
sessionID: "ses_1",
|
||||
client,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
info: session,
|
||||
messages,
|
||||
})
|
||||
})
|
||||
|
||||
test("throws when session not found", async () => {
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
fetchSessionExport({
|
||||
sessionID: "ses_missing",
|
||||
client,
|
||||
}),
|
||||
).rejects.toThrow("Session not found: ses_missing")
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
// Matches the exact `{ info, messages: [{ info, parts }] }` structure produced by `opencode export` CLI
|
||||
export type SessionExportData = {
|
||||
info: Session
|
||||
messages: {
|
||||
info: Message
|
||||
parts: Part[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export type SessionExportClient = {
|
||||
session: {
|
||||
get: (input: { sessionID: string }) => Promise<{ data?: Session | null }>
|
||||
messages: (input: { sessionID: string }) => Promise<{ data?: SessionExportData["messages"] | null }>
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSessionExport(input: {
|
||||
sessionID: string
|
||||
client: SessionExportClient
|
||||
}): Promise<SessionExportData> {
|
||||
const [sessionRes, messagesRes] = await Promise.all([
|
||||
input.client.session.get({ sessionID: input.sessionID }),
|
||||
input.client.session.messages({ sessionID: input.sessionID }),
|
||||
])
|
||||
|
||||
if (!sessionRes?.data) {
|
||||
throw new Error(`Session not found: ${input.sessionID}`)
|
||||
}
|
||||
if (!messagesRes?.data) {
|
||||
throw new Error(`Failed to load messages for session: ${input.sessionID}`)
|
||||
}
|
||||
|
||||
return {
|
||||
info: sessionRes.data,
|
||||
messages: messagesRes.data,
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionExportFilename(session: { id: string; title?: string; slug?: string }) {
|
||||
const name = session.title || session.slug || session.id
|
||||
const clean = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/gi, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return `${clean || session.id}.json`
|
||||
}
|
||||
|
||||
export function downloadSessionExport(filename: string, data: unknown) {
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "195K",
|
||||
full: "195,000",
|
||||
compact: "160K",
|
||||
full: "160,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "950",
|
||||
contributors: "900",
|
||||
commits: "13,000",
|
||||
monthlyUsers: "16M",
|
||||
monthlyUsers: "7.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -197,9 +197,7 @@ export async function handler(
|
||||
if (Array.isArray(v)) return [[k, v]]
|
||||
if (typeof v === "object") return [[k, replacer(v)]]
|
||||
if (typeof v === "string") {
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : []
|
||||
if (v === "$org")
|
||||
return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : []
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||
if (v === "$user") return stickyId ? [[k, stickyId]] : []
|
||||
if (v.startsWith("$header.")) {
|
||||
const headerValue = input.request.headers.get(v.slice(8))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -53,6 +53,7 @@ export const BillingTable = mysqlTable(
|
||||
...workspaceIndexes(table),
|
||||
uniqueIndex("global_customer_id").on(table.customerID),
|
||||
uniqueIndex("global_subscription_id").on(table.subscriptionID),
|
||||
uniqueIndex("global_lite_subscription_id").on(table.liteSubscriptionID),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { bigint, index, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
@@ -31,5 +31,8 @@ export const ReferralRewardTable = mysqlTable(
|
||||
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||
timeApplied: utc("time_applied"),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })],
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.workspaceID, table.referralID] }),
|
||||
index("referral_id").on(table.referralID),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -54,15 +54,15 @@ export default {
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
const lakeIngest = getLakeIngest()
|
||||
const [lake] = await Promise.all([
|
||||
// fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// "X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
// },
|
||||
// body: JSON.stringify(events),
|
||||
// }),
|
||||
const [honeycomb, lake] = await Promise.all([
|
||||
fetch("https://api.honeycomb.io/1/batch/zen", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
}),
|
||||
...(lakeIngest
|
||||
? [
|
||||
fetch(lakeIngest.url, {
|
||||
@@ -76,8 +76,8 @@ export default {
|
||||
]
|
||||
: []),
|
||||
])
|
||||
// console.log(honeycomb.status)
|
||||
// console.log(await honeycomb.text())
|
||||
console.log(honeycomb.status)
|
||||
console.log(await honeycomb.text())
|
||||
if (lake) {
|
||||
console.log(lake.status)
|
||||
console.log(await lake.text())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -5,7 +5,5 @@
|
||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for native menus, picker titles, dialogs, buttons, accessible labels, and displayed errors.
|
||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||
- Keep locale and grammar logic in the shared typed i18n layer. Renderer code should resolve copy through the app language API, and the main process should consume typed native-translation bundles through `nativeT(...)`; native menus, dialogs, and IPC handlers must not inspect locales, choose plural categories, or assemble translated sentence fragments.
|
||||
- Prefer complete translated phrases with only irreducible dynamic placeholders. If native UI needs richer grammar, deepen the shared bundle/API instead of adding locale branches to desktop feature code.
|
||||
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -189,11 +189,6 @@ async function writeZip(output: string, entries: Entry[]) {
|
||||
}
|
||||
|
||||
function initConsoleTransport() {
|
||||
if (app.isPackaged) {
|
||||
log.transports.console.level = false
|
||||
return
|
||||
}
|
||||
|
||||
const write = log.transports.console.writeFn.bind(log.transports.console)
|
||||
log.transports.console.writeFn = (options) => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -21,7 +21,6 @@ await Bun.build({
|
||||
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||
define: {
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
files: {
|
||||
|
||||
@@ -656,7 +656,7 @@ function makeUsageService(sdk: OpencodeClient) {
|
||||
sessionId: params.sessionID,
|
||||
update: {
|
||||
sessionUpdate: "usage_update",
|
||||
used: UsageService.contextTokens(message),
|
||||
used: message.tokens.input + message.tokens.cache.read,
|
||||
size,
|
||||
cost: { amount: UsageService.totalSessionCost(messages), currency: "USD" },
|
||||
},
|
||||
|
||||
@@ -83,10 +83,6 @@ export function messageLoaderFromSDK(sdk: SDK): MessageLoaderInterface {
|
||||
|
||||
export const messageLoaderLayer = (sdk: SDK) => Layer.succeed(MessageLoader, messageLoaderFromSDK(sdk))
|
||||
|
||||
export function contextTokens(message: AssistantTokenCost): number {
|
||||
return message.tokens.input + message.tokens.cache.read + message.tokens.cache.write
|
||||
}
|
||||
|
||||
export function buildUsage(message: AssistantTokenCost): Usage {
|
||||
const cachedReadTokens = message.tokens.cache.read
|
||||
const cachedWriteTokens = message.tokens.cache.write
|
||||
@@ -211,7 +207,7 @@ const layer = Layer.effect(
|
||||
sessionId: input.sessionID,
|
||||
update: {
|
||||
sessionUpdate: "usage_update",
|
||||
used: contextTokens(message),
|
||||
used: message.tokens.input + message.tokens.cache.read,
|
||||
size,
|
||||
cost: { amount: totalSessionCost(messages), currency: "USD" },
|
||||
},
|
||||
|
||||
@@ -365,7 +365,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
})
|
||||
const footer = shell.footer
|
||||
const rememberLocal = (commit: StreamCommit, after?: LocalReplayAnchor) => {
|
||||
state.localRows = [...state.localRows, { commit, after, created: Date.now() }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
state.localRows = [...state.localRows, { commit, after }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
}
|
||||
|
||||
const loadCatalog = async (): Promise<void> => {
|
||||
|
||||
@@ -266,7 +266,7 @@ export function replayLocalRows(
|
||||
rows: LocalReplayRow[],
|
||||
): StreamCommit[] {
|
||||
const persisted = new Set(messages.map((message) => message.info.id))
|
||||
return rows.reduce((out, local, index) => {
|
||||
return rows.reduce((out, local) => {
|
||||
const row = local.commit
|
||||
if (row.kind === "user" && row.messageID && persisted.has(row.messageID)) {
|
||||
return out
|
||||
@@ -317,35 +317,12 @@ export function replayLocalRows(
|
||||
return [...out.slice(0, after + 1), row, ...out.slice(after + 1)]
|
||||
}
|
||||
|
||||
const nextAnchor = rows
|
||||
.slice(index + 1)
|
||||
.find((next) => next.commit.messageID === row.messageID && next.after)?.after
|
||||
if (nextAnchor) {
|
||||
const before = out.findIndex((commit) =>
|
||||
nextAnchor.partID
|
||||
? commit.partID === nextAnchor.partID
|
||||
: commit.kind === nextAnchor.kind && commit.messageID === nextAnchor.messageID,
|
||||
)
|
||||
if (before !== -1) return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
const before = out.findIndex((commit) => commit.messageID && row.messageID! < commit.messageID)
|
||||
if (before === -1) {
|
||||
return [...out, row]
|
||||
}
|
||||
|
||||
if (local.created !== undefined) {
|
||||
const created = local.created
|
||||
const messageID = row.messageID
|
||||
const later = new Set(
|
||||
messages
|
||||
.filter(
|
||||
(message) =>
|
||||
message.info.time.created > created ||
|
||||
(message.info.time.created === created && message.info.id.localeCompare(messageID) > 0),
|
||||
)
|
||||
.map((message) => message.info.id),
|
||||
)
|
||||
const before = out.findIndex((commit) => commit.messageID && later.has(commit.messageID))
|
||||
if (before !== -1) return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
}
|
||||
|
||||
return [...out, row]
|
||||
return [...out.slice(0, before), row, ...out.slice(before)]
|
||||
}, commits)
|
||||
}
|
||||
|
||||
|
||||
@@ -332,7 +332,6 @@ export type LocalReplayAnchor = {
|
||||
export type LocalReplayRow = {
|
||||
commit: StreamCommit
|
||||
after?: LocalReplayAnchor
|
||||
created?: number
|
||||
}
|
||||
|
||||
// The public contract between the stream transport / prompt queue and
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
import { OAUTH_DUMMY_KEY } from "../auth"
|
||||
import { createServer } from "http"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
||||
|
||||
// Public Grok-CLI OAuth client.
|
||||
// Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
|
||||
// non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
|
||||
// for desktop OAuth flows. Source of truth: hermes-agent PR #26534.
|
||||
const CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
|
||||
const AUTHORIZE_URL = "https://auth.x.ai/oauth2/authorize"
|
||||
const TOKEN_URL = "https://auth.x.ai/oauth2/token"
|
||||
// RFC 8628 device authorization grant. Confirmed exposed by xAI's
|
||||
// /.well-known/openid-configuration as `device_authorization_endpoint`
|
||||
@@ -25,15 +30,51 @@ const DEVICE_CODE_SLOW_DOWN_INCREMENT_MS = 5_000
|
||||
const DEVICE_CODE_DEFAULT_EXPIRES_MS = 5 * 60 * 1000
|
||||
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3_000
|
||||
|
||||
// xAI rejects redirect_uris that don't match what was registered for the
|
||||
// Grok-CLI client. The host:port pair is part of the registration, so we have
|
||||
// to bind the loopback server to this exact port.
|
||||
const OAUTH_HOST = "127.0.0.1"
|
||||
const OAUTH_PORT = 56121
|
||||
const OAUTH_REDIRECT_PATH = "/callback"
|
||||
const REDIRECT_URI = `http://${OAUTH_HOST}:${OAUTH_PORT}${OAUTH_REDIRECT_PATH}`
|
||||
|
||||
// Refresh the access token a little before it actually expires so a single
|
||||
// long-running tool call doesn't have to recover from a mid-flight 401.
|
||||
const ACCESS_TOKEN_REFRESH_SKEW_MS = 120_000
|
||||
|
||||
interface XaiAuthPluginOptions {
|
||||
authorizeUrl?: string
|
||||
tokenUrl?: string
|
||||
deviceAuthorizationUrl?: string
|
||||
}
|
||||
|
||||
interface PkceCodes {
|
||||
verifier: string
|
||||
challenge: string
|
||||
}
|
||||
|
||||
async function generatePKCE(): Promise<PkceCodes> {
|
||||
const verifier = generateRandomString(64)
|
||||
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))
|
||||
return { verifier, challenge: base64UrlEncode(hash) }
|
||||
}
|
||||
|
||||
function generateRandomString(length: number): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
|
||||
.map((b) => chars[b % chars.length])
|
||||
.join("")
|
||||
}
|
||||
|
||||
function base64UrlEncode(buffer: ArrayBuffer): string {
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer))
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
||||
}
|
||||
|
||||
function generateState(): string {
|
||||
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
@@ -74,6 +115,55 @@ export function accessTokenIsExpiring(
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAuthorizeUrl(
|
||||
pkce: PkceCodes,
|
||||
state: string,
|
||||
nonce: string,
|
||||
options: XaiAuthPluginOptions = {},
|
||||
): string {
|
||||
// `plan=generic` opts the consent screen into xAI's generic OAuth plan tier;
|
||||
// without it, accounts.x.ai rejects loopback OAuth from non-allowlisted
|
||||
// clients. `referrer=opencode` lets xAI attribute opencode-originated
|
||||
// logins in their OAuth server logs (best-effort attribution while we
|
||||
// continue to reuse the Grok-CLI client_id).
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: SCOPE,
|
||||
code_challenge: pkce.challenge,
|
||||
code_challenge_method: "S256",
|
||||
state,
|
||||
nonce,
|
||||
plan: "generic",
|
||||
referrer: "opencode",
|
||||
})
|
||||
return `${options.authorizeUrl ?? AUTHORIZE_URL}?${params.toString()}`
|
||||
}
|
||||
|
||||
async function exchangeCodeForTokens(
|
||||
code: string,
|
||||
pkce: PkceCodes,
|
||||
options: XaiAuthPluginOptions = {},
|
||||
): Promise<TokenResponse> {
|
||||
const response = await fetch(options.tokenUrl ?? TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
client_id: CLIENT_ID,
|
||||
code_verifier: pkce.verifier,
|
||||
}).toString(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => "")
|
||||
throw new Error(`xAI token exchange failed (${response.status})${detail ? `: ${detail}` : ""}`)
|
||||
}
|
||||
return response.json() as Promise<TokenResponse>
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string, options: XaiAuthPluginOptions = {}): Promise<TokenResponse> {
|
||||
const response = await fetch(options.tokenUrl ?? TOKEN_URL, {
|
||||
method: "POST",
|
||||
@@ -112,7 +202,6 @@ export async function requestDeviceCode(options: XaiAuthPluginOptions = {}): Pro
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
scope: SCOPE,
|
||||
referrer: "opencode",
|
||||
}).toString(),
|
||||
})
|
||||
if (!response.ok) {
|
||||
@@ -196,6 +285,170 @@ export async function pollDeviceCodeToken(
|
||||
throw new Error("xAI device authorization timed out")
|
||||
}
|
||||
|
||||
// CORS allowlist for the loopback callback. The redirect_uri itself is
|
||||
// already bound to 127.0.0.1 and gated by PKCE+state, so we only accept
|
||||
// xAI's own auth origins for additional defense-in-depth on the OPTIONS
|
||||
// preflight.
|
||||
const CORS_ALLOWED_ORIGINS = new Set(["https://accounts.x.ai", "https://auth.x.ai"])
|
||||
|
||||
interface PendingOAuth {
|
||||
pkce: PkceCodes
|
||||
state: string
|
||||
resolve: (tokens: TokenResponse) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
let oauthServer: ReturnType<typeof createServer> | undefined
|
||||
let pendingOAuth: PendingOAuth | undefined
|
||||
|
||||
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
|
||||
if (oauthServer) return { port: OAUTH_PORT, redirectUri: REDIRECT_URI }
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const reqUrl = req.url || "/"
|
||||
const url = new URL(reqUrl, `http://${OAUTH_HOST}:${OAUTH_PORT}`)
|
||||
|
||||
const origin = req.headers["origin"]
|
||||
const allowOrigin = typeof origin === "string" && CORS_ALLOWED_ORIGINS.has(origin) ? origin : ""
|
||||
if (allowOrigin) {
|
||||
res.setHeader("Access-Control-Allow-Origin", allowOrigin)
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||
res.setHeader("Access-Control-Allow-Private-Network", "true")
|
||||
res.setHeader("Vary", "Origin")
|
||||
}
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === OAUTH_REDIRECT_PATH) {
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
const errorMsg = "Missing authorization code"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!pendingOAuth || state !== pendingOAuth.state) {
|
||||
const errorMsg = "Invalid state - potential CSRF attack"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
|
||||
return
|
||||
}
|
||||
|
||||
const current = pendingOAuth
|
||||
pendingOAuth = undefined
|
||||
|
||||
exchangeCodeForTokens(code, current.pkce)
|
||||
.then((tokens) => current.resolve(tokens))
|
||||
.catch((err) => current.reject(err))
|
||||
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(OauthCallbackPage.success({ provider: "xAI" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === "/cancel") {
|
||||
pendingOAuth?.reject(new Error("Login cancelled"))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(200)
|
||||
res.end("Login cancelled")
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(404)
|
||||
res.end("Not found")
|
||||
})
|
||||
|
||||
// listen() failures (e.g. EADDRINUSE because Grok-CLI is bound to the same
|
||||
// pinned port) must clear `oauthServer` and remove our error listener,
|
||||
// otherwise the next startOAuthServer() short-circuits on the truthy check
|
||||
// and returns a redirect_uri pointing at nothing.
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (err: Error) => {
|
||||
oauthServer = undefined
|
||||
reject(err)
|
||||
}
|
||||
server.once("error", onError)
|
||||
server.listen(OAUTH_PORT, OAUTH_HOST, () => {
|
||||
server.removeListener("error", onError)
|
||||
// After listen() succeeds, install a permanent log-only listener so
|
||||
// that subsequent server errors (e.g. accept() failures, socket-level
|
||||
// errors) don't trip Node's default "unhandled error event = throw"
|
||||
// behavior and crash the entire opencode process. Matches the silent-
|
||||
// swallow behavior the Codex plugin gets from its permanent
|
||||
// `oauthServer!.on("error", reject)`.
|
||||
resolve()
|
||||
})
|
||||
oauthServer = server
|
||||
})
|
||||
|
||||
return { port: OAUTH_PORT, redirectUri: REDIRECT_URI }
|
||||
}
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (oauthServer) {
|
||||
oauthServer.close()
|
||||
oauthServer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise<TokenResponse> {
|
||||
// A previous in-flight authorize() that the user abandoned (or that is
|
||||
// being superseded by a fresh attempt) still owns `pendingOAuth`. Reject
|
||||
// it eagerly so its caller stops waiting on a state value that can never
|
||||
// match the next callback.
|
||||
if (pendingOAuth) {
|
||||
pendingOAuth.reject(new Error("Superseded by a newer xAI authorize request"))
|
||||
pendingOAuth = undefined
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
if (pendingOAuth) {
|
||||
pendingOAuth = undefined
|
||||
reject(new Error("OAuth callback timeout - authorization took too long"))
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
)
|
||||
|
||||
pendingOAuth = {
|
||||
pkce,
|
||||
state,
|
||||
resolve: (tokens) => {
|
||||
clearTimeout(timeout)
|
||||
resolve(tokens)
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface RefreshResult {
|
||||
access: string
|
||||
refresh: string
|
||||
@@ -295,6 +548,40 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
}
|
||||
},
|
||||
methods: [
|
||||
{
|
||||
label: "xAI Grok OAuth (SuperGrok Subscription)",
|
||||
type: "oauth",
|
||||
authorize: async () => {
|
||||
await startOAuthServer()
|
||||
const pkce = await generatePKCE()
|
||||
const state = generateState()
|
||||
const nonce = generateState()
|
||||
const authUrl = buildAuthorizeUrl(pkce, state, nonce, options)
|
||||
|
||||
const callbackPromise = waitForOAuthCallback(pkce, state)
|
||||
|
||||
return {
|
||||
url: authUrl,
|
||||
instructions: "Complete authorization in your browser. This window will close automatically.",
|
||||
method: "auto" as const,
|
||||
callback: async () => {
|
||||
try {
|
||||
const tokens = await callbackPromise
|
||||
return {
|
||||
type: "success" as const,
|
||||
refresh: tokens.refresh_token,
|
||||
access: tokens.access_token,
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
}
|
||||
} catch (err) {
|
||||
return { type: "failed" as const }
|
||||
} finally {
|
||||
stopOAuthServer()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// RFC 8628 device-code flow. The CLI prints a verification URL
|
||||
// and a short user_code that the user enters in a browser on
|
||||
@@ -304,7 +591,7 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
|
||||
// user's browser. Defends the only attack surface (the polling
|
||||
// loop) with the standard authorization_pending / slow_down
|
||||
// backoff and a hard deadline from xAI's `expires_in`.
|
||||
label: "SuperGrok Subscription",
|
||||
label: "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||
type: "oauth",
|
||||
authorize: async () => {
|
||||
const device = await requestDeviceCode(options)
|
||||
|
||||
@@ -9,7 +9,7 @@ let embeddedUIPromise: Promise<Record<string, string> | null> | undefined
|
||||
export const UI_UPSTREAM = new URL("https://app.opencode.ai")
|
||||
|
||||
export const csp = (hash = "") =>
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data:; media-src 'self' data:; connect-src * data: blob:`
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:`
|
||||
export const DEFAULT_CSP = csp()
|
||||
|
||||
export function themePreloadHash(body: string) {
|
||||
|
||||
@@ -49,42 +49,6 @@ type CompletedCompaction = {
|
||||
summary: string | undefined
|
||||
}
|
||||
|
||||
const truncate = (value: string) =>
|
||||
value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]`
|
||||
|
||||
const serialize = (message: SessionV1.WithParts) => {
|
||||
if (message.info.role === "user") {
|
||||
const text = message.parts
|
||||
.filter((part): part is SessionV1.TextPart => part.type === "text" && !part.ignored)
|
||||
.map((part) => part.text)
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
const files = message.parts.flatMap((part) =>
|
||||
part.type === "file" ? [`[Attached ${part.mime}: ${part.filename ?? "file"}]`] : [],
|
||||
)
|
||||
return [...(text ? [`[User]: ${text}`] : []), ...files].join("\n")
|
||||
}
|
||||
return message.parts
|
||||
.flatMap((part) => {
|
||||
if (part.type === "text") return part.text ? [`[Assistant]: ${part.text}`] : []
|
||||
if (part.type === "reasoning") return part.text ? [`[Assistant reasoning]: ${part.text}`] : []
|
||||
if (part.type !== "tool") return []
|
||||
const call = `[Assistant tool call]: ${part.tool}(${JSON.stringify(part.state.input)})`
|
||||
if (part.state.status === "completed") {
|
||||
const attachments = (part.state.attachments ?? []).map(
|
||||
(item) => `[Attached ${item.mime}: ${item.filename ?? "file"}]`,
|
||||
)
|
||||
const output = part.state.time.compacted
|
||||
? "[Old tool result content cleared]"
|
||||
: truncate([part.state.output, ...attachments].join("\n"))
|
||||
return [call, `[Tool result]: ${output}`]
|
||||
}
|
||||
if (part.state.status === "error") return [call, `[Tool error]: ${part.state.error}`]
|
||||
return [call]
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function summaryText(message: SessionV1.WithParts) {
|
||||
const text = message.parts
|
||||
.filter((part): part is SessionV1.TextPart => part.type === "text")
|
||||
@@ -384,7 +348,10 @@ const layer = Layer.effect(
|
||||
const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
|
||||
const msgs = structuredClone(selected.head)
|
||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||
const conversation = msgs.map(serialize).filter(Boolean).join("\n\n")
|
||||
const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
|
||||
stripMedia: true,
|
||||
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
|
||||
})
|
||||
const ctx = yield* InstanceState.context
|
||||
const msg: SessionV1.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
@@ -425,16 +392,10 @@ const layer = Layer.effect(
|
||||
tools: {},
|
||||
system: [],
|
||||
messages: [
|
||||
...modelMessages,
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [nextPrompt, "The following is the conversation history:", conversation]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
},
|
||||
],
|
||||
content: [{ type: "text", text: nextPrompt }],
|
||||
},
|
||||
],
|
||||
model,
|
||||
|
||||
@@ -636,14 +636,30 @@ const layer = Layer.effect(
|
||||
yield* Effect.gen(function* () {
|
||||
ctx.currentText = undefined
|
||||
ctx.reasoningMap = {}
|
||||
let generated = false
|
||||
yield* status.set(ctx.sessionID, { type: "busy" })
|
||||
const stream = llm.stream(streamInput)
|
||||
|
||||
yield* stream.pipe(
|
||||
Stream.tap((event) => handleEvent(event)),
|
||||
Stream.tap((event) => {
|
||||
if (
|
||||
(event.type === "text-delta" && event.text.length > 0) ||
|
||||
(event.type === "reasoning-delta" && event.text.length > 0) ||
|
||||
event.type === "tool-input-start" ||
|
||||
event.type === "tool-call"
|
||||
) {
|
||||
generated = true
|
||||
}
|
||||
return handleEvent(event)
|
||||
}),
|
||||
Stream.takeUntil(() => ctx.needsCompaction),
|
||||
Stream.runDrain,
|
||||
)
|
||||
if (ctx.assistantMessage.finish === "unknown" && !generated) {
|
||||
yield* new SessionRetry.EmptyResponseError({
|
||||
message: "The model returned an empty response with an unknown finish reason",
|
||||
})
|
||||
}
|
||||
}).pipe(
|
||||
Effect.onInterrupt(() =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
|
||||
import { Cause, Clock, Duration, Effect, Schedule, Schema } from "effect"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { iife } from "@/util/iife"
|
||||
import { isRecord } from "@/util/record"
|
||||
@@ -23,20 +23,15 @@ export type Retryable = {
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyResponseError extends Schema.TaggedErrorClass<EmptyResponseError>()("SessionEmptyResponseError", {
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const RETRY_INITIAL_DELAY = 2000
|
||||
export const RETRY_BACKOFF_FACTOR = 2
|
||||
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
|
||||
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
|
||||
|
||||
const RETRYABLE_MESSAGE_PATTERNS = [
|
||||
/429|500|502|503|504|524/i,
|
||||
/rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i,
|
||||
/overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i,
|
||||
/terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i,
|
||||
/^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
|
||||
/try your request again|retry your request|resource exhausted|resource_exhausted/i,
|
||||
]
|
||||
|
||||
function cap(ms: number) {
|
||||
return Math.min(ms, RETRY_MAX_DELAY)
|
||||
}
|
||||
@@ -81,13 +76,7 @@ export function retryable(error: Err, provider: string) {
|
||||
const status = error.data.statusCode
|
||||
// 5xx errors are transient server failures and should always be retried,
|
||||
// even when the provider SDK doesn't explicitly mark them as retryable.
|
||||
if (
|
||||
!error.data.isRetryable &&
|
||||
!(status !== undefined && status >= 500) &&
|
||||
!matchesRetryableMessage(error.data.message) &&
|
||||
!matchesRetryableMessage(error.data.responseBody)
|
||||
)
|
||||
return undefined
|
||||
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
|
||||
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
|
||||
return {
|
||||
message: GO_UPSELL_MESSAGE,
|
||||
@@ -137,17 +126,33 @@ export function retryable(error: Err, provider: string) {
|
||||
return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message }
|
||||
}
|
||||
|
||||
const message = isRecord(error.data) ? error.data.message : undefined
|
||||
if (typeof message !== "string") return undefined
|
||||
const lower = message.toLowerCase()
|
||||
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
|
||||
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
|
||||
if (matchesRetryableMessage(message)) return { message }
|
||||
return undefined
|
||||
}
|
||||
// Check for rate limit patterns in plain text error messages
|
||||
const msg = isRecord(error.data) ? error.data.message : undefined
|
||||
if (typeof msg === "string") {
|
||||
const lower = msg.toLowerCase()
|
||||
if (
|
||||
lower.includes("rate increased too quickly") ||
|
||||
lower.includes("rate limit") ||
|
||||
lower.includes("too many requests")
|
||||
) {
|
||||
return { message: msg }
|
||||
}
|
||||
}
|
||||
|
||||
function matchesRetryableMessage(value: unknown) {
|
||||
return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value))
|
||||
const json = parseJSON(msg)
|
||||
if (!json || typeof json !== "object") return undefined
|
||||
const code = typeof json.code === "string" ? json.code : ""
|
||||
|
||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||
return { message: "Too Many Requests" }
|
||||
}
|
||||
if (code.includes("exhausted") || code.includes("unavailable")) {
|
||||
return { message: "Provider is overloaded" }
|
||||
}
|
||||
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
|
||||
return { message: "Rate Limited" }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function str(value: unknown) {
|
||||
@@ -180,7 +185,8 @@ export function policy(opts: {
|
||||
return Schedule.fromStepWithMetadata(
|
||||
Effect.succeed((meta: Schedule.InputMetadata<unknown>) => {
|
||||
const error = opts.parse(meta.input)
|
||||
const retry = retryable(error, opts.provider)
|
||||
const retry =
|
||||
meta.input instanceof EmptyResponseError ? { message: meta.input.message } : retryable(error, opts.provider)
|
||||
if (!retry) return Cause.done(meta.attempt)
|
||||
return Effect.gen(function* () {
|
||||
const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("acp usage", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("includes cache reads and writes in ACP context usage", () => {
|
||||
it.effect("sends ACP usage_update with context size and cumulative assistant cost", () => {
|
||||
const updates: SessionNotification[] = []
|
||||
return Effect.gen(function* () {
|
||||
const usage = yield* UsageService.Service
|
||||
@@ -222,7 +222,7 @@ describe("acp usage", () => {
|
||||
sessionId: "ses_1",
|
||||
update: {
|
||||
sessionUpdate: "usage_update",
|
||||
used: 22,
|
||||
used: 15,
|
||||
size: 128_000,
|
||||
cost: { amount: 3, currency: "USD" },
|
||||
},
|
||||
@@ -239,7 +239,7 @@ describe("acp usage", () => {
|
||||
input: 10,
|
||||
output: 20,
|
||||
reasoning: 0,
|
||||
cache: { read: 5, write: 7 },
|
||||
cache: { read: 5, write: 0 },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -81,11 +81,11 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
30_000,
|
||||
)
|
||||
|
||||
// The test provider's SSE error item is interpreted by the SDK as an unknown
|
||||
// finish, not a fatal provider/session error. Lock that distinction in so it
|
||||
// is not accidentally used as the failure compatibility oracle.
|
||||
// The test provider's SSE error item is interpreted by the SDK as an empty
|
||||
// response with an unknown finish. That attempt should retry while preserving
|
||||
// output from the preceding tool-call step.
|
||||
cliIt.concurrent(
|
||||
"unknown stream finish preserves partial output and exits 0",
|
||||
"empty unknown stream finish retries and preserves partial output",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* llm.push(
|
||||
@@ -95,9 +95,10 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
}),
|
||||
)
|
||||
yield* llm.fail("upstream provider exploded mid-stream")
|
||||
yield* llm.text("recovered response")
|
||||
const result = yield* opencode.run("trigger midstream error", { timeoutMs: 30_000 })
|
||||
expect(result.exitCode).toBe(0)
|
||||
expect(result.stdout).toBe("partial response\n")
|
||||
expect(result.stdout).toBe("partial response\nrecovered response\n")
|
||||
expect(result.stderr).not.toContain("upstream provider exploded mid-stream")
|
||||
}),
|
||||
60_000,
|
||||
@@ -213,7 +214,7 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
)
|
||||
|
||||
cliIt.concurrent(
|
||||
"--format json records partial output for an unknown stream finish",
|
||||
"--format json records an empty unknown stream retry",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* llm.push(
|
||||
@@ -223,6 +224,7 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
}),
|
||||
)
|
||||
yield* llm.fail("provider failed")
|
||||
yield* llm.text("recovered json")
|
||||
const result = yield* opencode.run("fail after output", { format: "json" })
|
||||
|
||||
const events = opencode.parseJsonEvents(result.stdout)
|
||||
@@ -234,9 +236,13 @@ describe("opencode run (non-interactive subprocess)", () => {
|
||||
"step_finish",
|
||||
"step_start",
|
||||
"step_finish",
|
||||
"step_start",
|
||||
"text",
|
||||
"step_finish",
|
||||
])
|
||||
expect(events[1]?.part).toEqual(expect.objectContaining({ type: "text", text: "partial json" }))
|
||||
expect(events.at(-1)?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "unknown" }))
|
||||
expect(events.at(-2)?.part).toEqual(expect.objectContaining({ type: "text", text: "recovered json" }))
|
||||
expect(events.at(-1)?.part).toEqual(expect.objectContaining({ type: "step-finish", reason: "stop" }))
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
@@ -416,14 +416,7 @@ describe("run session replay", () => {
|
||||
} as const
|
||||
|
||||
expect(
|
||||
replayLocalRows(
|
||||
[userMessage("msg-user-2", "successful")],
|
||||
[persisted],
|
||||
[
|
||||
{ commit: failed, created: 0 },
|
||||
{ commit: error, created: 0 },
|
||||
],
|
||||
),
|
||||
replayLocalRows([userMessage("msg-user-2", "successful")], [persisted], [{ commit: failed }, { commit: error }]),
|
||||
).toEqual([failed, error, persisted])
|
||||
})
|
||||
|
||||
@@ -664,35 +657,35 @@ describe("run session replay", () => {
|
||||
).toEqual([prompt, running, error, completed])
|
||||
})
|
||||
|
||||
test("appends an unanchored local diagnostic without inferring chronology from message IDs", () => {
|
||||
test("retains an unpersisted local diagnostic before later persisted prompts", () => {
|
||||
const first = {
|
||||
kind: "user",
|
||||
text: "before",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-z",
|
||||
messageID: "msg-user-1",
|
||||
} as const
|
||||
const error = {
|
||||
kind: "error",
|
||||
text: "failed to start new session",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-m",
|
||||
messageID: "msg-user-2",
|
||||
} as const
|
||||
const second = {
|
||||
kind: "user",
|
||||
text: "after",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-a",
|
||||
messageID: "msg-user-3",
|
||||
} as const
|
||||
|
||||
expect(
|
||||
replayLocalRows(
|
||||
[userMessage("msg-user-z", "before"), userMessage("msg-user-a", "after")],
|
||||
[userMessage("msg-user-1", "before"), userMessage("msg-user-3", "after")],
|
||||
[first, second],
|
||||
[{ commit: error }],
|
||||
),
|
||||
).toEqual([first, second, error])
|
||||
).toEqual([first, error, second])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { accessTokenIsExpiring, pollDeviceCodeToken, requestDeviceCode, XaiAuthPlugin } from "../../src/plugin/xai"
|
||||
import {
|
||||
accessTokenIsExpiring,
|
||||
buildAuthorizeUrl,
|
||||
pollDeviceCodeToken,
|
||||
requestDeviceCode,
|
||||
XaiAuthPlugin,
|
||||
} from "../../src/plugin/xai"
|
||||
import { OAUTH_DUMMY_KEY } from "../../src/auth"
|
||||
|
||||
function makeJwt(payload: object): string {
|
||||
@@ -70,6 +76,32 @@ describe("plugin.xai", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAuthorizeUrl", () => {
|
||||
const pkce = { verifier: "ver", challenge: "chal" }
|
||||
|
||||
test("includes required OAuth + PKCE + OIDC params", () => {
|
||||
const url = new URL(buildAuthorizeUrl(pkce, "state-abc", "nonce-xyz"))
|
||||
const params = url.searchParams
|
||||
|
||||
expect(url.origin + url.pathname).toBe("https://auth.x.ai/oauth2/authorize")
|
||||
expect(params.get("response_type")).toBe("code")
|
||||
expect(params.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828")
|
||||
expect(params.get("redirect_uri")).toBe("http://127.0.0.1:56121/callback")
|
||||
expect(params.get("scope")).toBe("openid profile email offline_access grok-cli:access api:access")
|
||||
expect(params.get("code_challenge")).toBe("chal")
|
||||
expect(params.get("code_challenge_method")).toBe("S256")
|
||||
expect(params.get("state")).toBe("state-abc")
|
||||
expect(params.get("nonce")).toBe("nonce-xyz")
|
||||
expect(params.get("plan")).toBe("generic")
|
||||
expect(params.get("referrer")).toBe("opencode")
|
||||
})
|
||||
|
||||
test("supports endpoint override for local integration tests", () => {
|
||||
const url = new URL(buildAuthorizeUrl(pkce, "s", "n", { authorizeUrl: "http://127.0.0.1/oauth2/authorize" }))
|
||||
expect(url.origin + url.pathname).toBe("http://127.0.0.1/oauth2/authorize")
|
||||
})
|
||||
})
|
||||
|
||||
describe("loader", () => {
|
||||
test("returns no options unless stored auth is OAuth and exposes methods in order", async () => {
|
||||
const hooks = await XaiAuthPlugin({} as any)
|
||||
@@ -78,7 +110,8 @@ describe("plugin.xai", () => {
|
||||
await hooks.auth!.loader!(async () => ({ type: "wellknown", key: "k", token: "t" }) as any, {} as any),
|
||||
).toEqual({})
|
||||
expect(hooks.auth!.methods.map((m) => [m.type, m.label])).toEqual([
|
||||
["oauth", "SuperGrok Subscription"],
|
||||
["oauth", "xAI Grok OAuth (SuperGrok Subscription)"],
|
||||
["oauth", "xAI Grok OAuth (Headless / Remote / VPS)"],
|
||||
["api", "Manually enter API Key"],
|
||||
])
|
||||
})
|
||||
@@ -392,7 +425,8 @@ describe("plugin.xai", () => {
|
||||
})
|
||||
const hooks = await XaiAuthPlugin({} as any, serverOptions(server))
|
||||
const headless = hooks.auth!.methods.find(
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||
)!
|
||||
const result = await headless.authorize!()
|
||||
|
||||
@@ -415,7 +449,8 @@ describe("plugin.xai", () => {
|
||||
return new Response("unexpected request", { status: 500 })
|
||||
})
|
||||
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||
)!
|
||||
expect((await headless.authorize!()).url).toBe("https://x.ai/device")
|
||||
})
|
||||
@@ -439,7 +474,6 @@ describe("plugin.xai", () => {
|
||||
expect(parsed.get("scope")).toContain("offline_access")
|
||||
expect(parsed.get("scope")).toContain("grok-cli:access")
|
||||
expect(parsed.get("scope")).toContain("api:access")
|
||||
expect(parsed.get("referrer")).toBe("opencode")
|
||||
await expect(
|
||||
requestDeviceCode({ deviceAuthorizationUrl: new URL("/error", server.url).toString() }),
|
||||
).rejects.toThrow(/429.*rate limited/)
|
||||
@@ -577,7 +611,8 @@ describe("plugin.xai", () => {
|
||||
return Response.json({ error: "access_denied" }, { status: 400 })
|
||||
})
|
||||
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
|
||||
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||
)!
|
||||
expect(await ((await headless.authorize!()) as any).callback()).toEqual({ type: "failed" })
|
||||
})
|
||||
|
||||
@@ -326,7 +326,7 @@ describe("HttpApi UI fallback", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("allows embedded UI terminal wasm, blob attachments, and theme preload CSP", () =>
|
||||
it.live("allows embedded UI terminal wasm and theme preload CSP", () =>
|
||||
Effect.gen(function* () {
|
||||
const script = 'document.documentElement.dataset.theme = "dark"'
|
||||
|
||||
@@ -351,8 +351,7 @@ describe("HttpApi UI fallback", () => {
|
||||
const csp = response.headers.get("content-security-policy") ?? ""
|
||||
expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'")
|
||||
expect(csp).toContain(`'sha256-${createHash("sha256").update(script).digest("base64")}'`)
|
||||
expect(csp).toContain("img-src 'self' data: https: blob:")
|
||||
expect(csp).toContain("connect-src * data: blob:")
|
||||
expect(csp).toContain("connect-src * data:")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1362,10 +1362,10 @@ describe("session.compaction.process", () => {
|
||||
"summarizes only the head while keeping recent tail out of summary input",
|
||||
() => {
|
||||
const stub = llm()
|
||||
let messages: LLM.StreamInput["messages"] = []
|
||||
let captured = ""
|
||||
stub.push(
|
||||
reply("summary", (input) => {
|
||||
messages = input.messages
|
||||
captured = JSON.stringify(input.messages)
|
||||
}),
|
||||
)
|
||||
return Effect.gen(function* () {
|
||||
@@ -1386,10 +1386,7 @@ describe("session.compaction.process", () => {
|
||||
auto: false,
|
||||
})
|
||||
|
||||
const captured = JSON.stringify(messages)
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0]?.role).toBe("user")
|
||||
expect(captured).toContain("[User]: older context")
|
||||
expect(captured).toContain("older context")
|
||||
expect(captured).not.toContain("keep this turn")
|
||||
expect(captured).not.toContain("and this one too")
|
||||
expect(captured).not.toContain("What did we do so far?")
|
||||
@@ -1440,74 +1437,6 @@ describe("session.compaction.process", () => {
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
itCompaction.instance(
|
||||
"serializes repeated compaction history as one user message",
|
||||
() => {
|
||||
const stub = llm()
|
||||
let captured: LLM.StreamInput["messages"] = []
|
||||
stub.push(
|
||||
reply("summary two", (input) => {
|
||||
captured = input.messages
|
||||
}),
|
||||
)
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const ssn = yield* SessionNs.Service
|
||||
const test = yield* TestInstance
|
||||
const session = yield* ssn.create({})
|
||||
const turn = yield* createUserMessage(session.id, "original request")
|
||||
const kept = yield* createAssistantMessage(session.id, turn.id, test.directory)
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: kept.id,
|
||||
sessionID: session.id,
|
||||
type: "tool",
|
||||
callID: "read-call",
|
||||
tool: "read",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { filePath: "src/index.ts" },
|
||||
output: "file contents",
|
||||
title: "src/index.ts",
|
||||
metadata: {},
|
||||
time: { start: Date.now(), end: Date.now() },
|
||||
},
|
||||
})
|
||||
|
||||
const previous = yield* ssn.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
model: ref,
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
yield* ssn.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: previous.id,
|
||||
sessionID: session.id,
|
||||
type: "compaction",
|
||||
auto: false,
|
||||
tail_start_id: kept.id,
|
||||
})
|
||||
yield* createSummaryAssistantMessage(session.id, previous.id, test.directory, "summary one")
|
||||
yield* createCompactionMarker(session.id)
|
||||
|
||||
const msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
const parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||
|
||||
expect(captured).toHaveLength(1)
|
||||
expect(captured[0]?.role).toBe("user")
|
||||
expect(JSON.stringify(captured)).toContain('[Assistant tool call]: read({\\"filePath\\":\\"src/index.ts\\"})')
|
||||
expect(JSON.stringify(captured)).toContain("[Tool result]: file contents")
|
||||
expect(JSON.stringify(captured)).not.toContain('\\"role\\":\\"assistant\\"')
|
||||
}).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 0 }) }))
|
||||
},
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => {
|
||||
const stub = llm()
|
||||
stub.push(reply("summary one"))
|
||||
|
||||
@@ -604,17 +604,32 @@ it.live("session.processor effect tests retry recognized structured json errors"
|
||||
),
|
||||
)
|
||||
|
||||
it.live("session.processor effect tests retry OpenAI-compatible midstream server errors", () =>
|
||||
it.live("session.processor effect tests retry empty responses with unknown finish reasons", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.push(raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }))
|
||||
yield* llm.text("after")
|
||||
yield* llm.push(
|
||||
raw({
|
||||
chunks: [
|
||||
{
|
||||
id: "chatcmpl-test",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ delta: { role: "assistant" }, finish_reason: null }],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-test",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ delta: {}, finish_reason: "unknown_reason" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
reply().text("after").stop(),
|
||||
)
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "retry midstream server error")
|
||||
const parent = yield* user(chat.id, "retry empty")
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const handle = yield* processors.create({
|
||||
@@ -636,7 +651,7 @@ it.live("session.processor effect tests retry OpenAI-compatible midstream server
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "retry midstream server error" }],
|
||||
messages: [{ role: "user", content: "retry empty" }],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
|
||||
@@ -118,21 +118,16 @@ describe("session.retry.delay", () => {
|
||||
})
|
||||
|
||||
describe("session.retry.retryable", () => {
|
||||
test("retries serialized too_many_requests messages", () => {
|
||||
test("maps too_many_requests json messages", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Too Many Requests" })
|
||||
})
|
||||
|
||||
test("retries serialized overloaded provider codes", () => {
|
||||
test("maps overloaded provider codes", () => {
|
||||
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" })
|
||||
})
|
||||
|
||||
test("retries serialized rate_limit messages", () => {
|
||||
const message = JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } })
|
||||
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
|
||||
})
|
||||
|
||||
test("does not retry unknown json messages", () => {
|
||||
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
|
||||
@@ -168,45 +163,6 @@ describe("session.retry.retryable", () => {
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg })
|
||||
})
|
||||
|
||||
test.each([
|
||||
"Internal server error",
|
||||
"internal error",
|
||||
"server-error",
|
||||
"Provider returned error",
|
||||
"provider-returned-error",
|
||||
"terminated",
|
||||
"fetch failed",
|
||||
"connection refused",
|
||||
"connect ECONNREFUSED",
|
||||
"request ETIMEDOUT",
|
||||
"failed to fetch",
|
||||
"EAI_AGAIN",
|
||||
"response timed out",
|
||||
"Please retry your request",
|
||||
"try your request again",
|
||||
"upstream returned status 524",
|
||||
])("retries matching API error text: %s", (message) => {
|
||||
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
|
||||
})
|
||||
|
||||
test("retries hyphenated service-unavailable errors", () => {
|
||||
expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({
|
||||
message: "Provider is overloaded",
|
||||
})
|
||||
})
|
||||
|
||||
test("matches retryable API response bodies", () => {
|
||||
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
||||
new SessionV1.APIError({
|
||||
message: "Request failed",
|
||||
isRetryable: false,
|
||||
statusCode: 400,
|
||||
responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }),
|
||||
}).toObject(),
|
||||
)
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" })
|
||||
})
|
||||
|
||||
test("retries transport timeout errors", () => {
|
||||
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
|
||||
expect(SessionV1.APIError.isInstance(request)).toBe(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.14",
|
||||
"version": "1.18.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -204,7 +204,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
>
|
||||
<TextShimmer text={title().title} active={pending()} />
|
||||
</span>
|
||||
<Show when={!pending() || title().subtitle || title().args?.length}>
|
||||
<Show when={!pending()}>
|
||||
<Show when={title().subtitle}>
|
||||
<span
|
||||
data-slot="basic-tool-tool-subtitle"
|
||||
|
||||
@@ -61,7 +61,6 @@ import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
|
||||
import { AnimatedCountList } from "./tool-count-summary"
|
||||
import { ToolStatusTitle } from "./tool-status-title"
|
||||
import { patchFiles } from "./apply-patch-file"
|
||||
import { partDefaultOpen } from "./part-default-open"
|
||||
import { animate } from "motion"
|
||||
import { attached, inline, kind, typeLabel } from "./message-file"
|
||||
import { readPartText } from "./message-part-text"
|
||||
@@ -719,7 +718,15 @@ export function renderable(part: PartType, showReasoningSummaries = true) {
|
||||
return !!PART_MAPPING[part.type]
|
||||
}
|
||||
|
||||
export { partDefaultOpen } from "./part-default-open"
|
||||
function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
||||
if (tool === "bash" || tool === "shell") return shell
|
||||
if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit
|
||||
}
|
||||
|
||||
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
|
||||
if (part.type !== "tool") return
|
||||
return toolDefaultOpen(part.tool, shell, edit)
|
||||
}
|
||||
|
||||
export function AssistantParts(props: {
|
||||
messages: AssistantMessage[]
|
||||
@@ -1127,10 +1134,10 @@ export function ContextToolGroup(props: {
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer text={trigger().title} active={running()} />
|
||||
</span>
|
||||
<Show when={trigger().subtitle}>
|
||||
<Show when={!running() && trigger().subtitle}>
|
||||
<span data-slot="basic-tool-tool-subtitle">{trigger().subtitle}</span>
|
||||
</Show>
|
||||
<Show when={trigger().args?.length}>
|
||||
<Show when={!running() && trigger().args?.length}>
|
||||
<For each={trigger().args}>
|
||||
{(arg) => <span data-slot="basic-tool-tool-arg">{arg}</span>}
|
||||
</For>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Part as PartType } from "@opencode-ai/sdk/v2"
|
||||
import { partDefaultOpen } from "./part-default-open"
|
||||
|
||||
describe("partDefaultOpen", () => {
|
||||
test("keeps edited files expanded when enabled", () => {
|
||||
expect(partDefaultOpen(tool("edit", { filediff: { additions: 1, deletions: 1 } }), false, true)).toBe(true)
|
||||
})
|
||||
|
||||
test("collapses deletion-only edits when enabled", () => {
|
||||
expect(partDefaultOpen(tool("edit", { filediff: { additions: 0, deletions: 1_200 } }), false, true)).toBe(false)
|
||||
})
|
||||
|
||||
test("collapses patches containing only deleted files when enabled", () => {
|
||||
expect(
|
||||
partDefaultOpen(
|
||||
tool("apply_patch", {
|
||||
files: [
|
||||
{ filePath: "one.ts", type: "delete" },
|
||||
{ filePath: "two.ts", type: "delete" },
|
||||
],
|
||||
}),
|
||||
false,
|
||||
true,
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test("keeps mixed patches expanded when enabled", () => {
|
||||
expect(
|
||||
partDefaultOpen(
|
||||
tool("apply_patch", {
|
||||
files: [
|
||||
{ filePath: "one.ts", type: "delete" },
|
||||
{ filePath: "two.ts", type: "update" },
|
||||
],
|
||||
}),
|
||||
false,
|
||||
true,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves shell defaults", () => {
|
||||
expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
function tool(name: string, metadata: Record<string, unknown>): PartType {
|
||||
return {
|
||||
id: `part_${name}`,
|
||||
sessionID: "session",
|
||||
messageID: "message",
|
||||
type: "tool",
|
||||
callID: `call_${name}`,
|
||||
tool: name,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "",
|
||||
title: name,
|
||||
metadata,
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user